Refactor + a few improvements to UI #8
|
@ -79,7 +79,7 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
LOGGER.info("Launching...");
|
LOGGER.info("Launching...");
|
||||||
plugins.forEach(FrogPlugin::run);
|
plugins.forEach(FrogPlugin::run);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
LoaderGui.builder().setContent(LoaderGui.ContentType.GENERIC_ERROR, t).addReport(CrashReportGenerator.writeReport(t, collectMods().values())).build().show();
|
LoaderGui.execReport(CrashReportGenerator.writeReport(t, collectMods().values()), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,281 +1,136 @@
|
||||||
package dev.frogmc.frogloader.impl.gui;
|
package dev.frogmc.frogloader.impl.gui;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.plaf.basic.BasicBorders;
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionListener;
|
||||||
import java.io.PrintWriter;
|
import java.awt.event.WindowAdapter;
|
||||||
import java.io.StringWriter;
|
import java.awt.event.WindowEvent;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
import dev.frogmc.frogloader.impl.gui.page.BreakingDepPage;
|
||||||
|
import dev.frogmc.frogloader.impl.gui.page.ReportPage;
|
||||||
|
import dev.frogmc.frogloader.impl.gui.page.UnfulfilledDepPage;
|
||||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
import dev.frogmc.frogloader.impl.util.PlatformUtil;
|
||||||
import dev.frogmc.frogloader.impl.util.URLUtil;
|
import org.slf4j.Logger;
|
||||||
import lombok.AccessLevel;
|
import org.slf4j.LoggerFactory;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
public class LoaderGui extends JFrame {
|
||||||
public class LoaderGui {
|
|
||||||
|
|
||||||
private final JFrame frame;
|
private static final Logger LOGGER = LoggerFactory.getLogger(LoaderGui.class);
|
||||||
private final boolean keepRunning;
|
|
||||||
|
|
||||||
public static Builder builder() {
|
private final JLabel header;
|
||||||
return new Builder();
|
private final JTabbedPane tabbedPane;
|
||||||
|
private final JPanel actions;
|
||||||
|
|
||||||
|
private LoaderGui() {
|
||||||
|
String title = "FrogLoader";
|
||||||
|
String version = LoaderGui.class.getPackage().getImplementationVersion();
|
||||||
|
if (version != null)
|
||||||
|
title += ' ' + version;
|
||||||
|
|
||||||
|
setTitle(title);
|
||||||
|
|
||||||
|
setSize(952, 560);
|
||||||
|
setLocationRelativeTo(null);
|
||||||
|
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||||
|
|
||||||
|
this.header = new JLabel();
|
||||||
|
this.header.setFont(this.header.getFont().deriveFont(Font.BOLD, this.header.getFont().getSize() * 2));
|
||||||
|
this.header.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||||
|
add(this.header, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
this.tabbedPane = new JTabbedPane();
|
||||||
|
add(this.tabbedPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
this.actions = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
|
||||||
|
JButton joinDiscordButton = new JButton("Discord Server");
|
||||||
|
joinDiscordButton.addActionListener(event -> PlatformUtil.open(URI.create("https://discord.frogmc.dev")));
|
||||||
|
this.actions.add(joinDiscordButton);
|
||||||
|
|
||||||
|
JButton exitButton = new JButton("Exit");
|
||||||
|
exitButton.addActionListener(event -> dispose());
|
||||||
|
this.actions.add(exitButton);
|
||||||
|
|
||||||
|
add(this.actions, BorderLayout.SOUTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JFrame getFrame(Path report) {
|
public void setHeader(String text) {
|
||||||
var frame = new JFrame();
|
this.header.setText(text);
|
||||||
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
|
}
|
||||||
frame.setSize(952, 560);
|
|
||||||
frame.setLocationRelativeTo(null);
|
|
||||||
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
|
||||||
|
|
||||||
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
public void addTab(String name, Component component) {
|
||||||
if (report != null) {
|
this.tabbedPane.add(name, component);
|
||||||
actions.add(new JButton(new AbstractAction("Open Report") {
|
}
|
||||||
|
|
||||||
|
public void addAction(String label, ActionListener listener) {
|
||||||
|
var button = new JButton(label);
|
||||||
|
button.addActionListener(listener);
|
||||||
|
this.actions.add(button, this.actions.getComponentCount() - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void execUnfulfilledDep(Path reportPath, ModDependencyResolver.UnfulfilledDependencyException ex, boolean keepRunning) {
|
||||||
|
exec(gui -> {
|
||||||
|
gui.setHeader("Found " + ex.getDependencies().size() + " problems");
|
||||||
|
gui.addTab("Info", new UnfulfilledDepPage(ex));
|
||||||
|
addReport(gui, reportPath);
|
||||||
|
}, keepRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void execBreakingDep(Path reportPath, ModDependencyResolver.BreakingModException ex, boolean keepRunning) {
|
||||||
|
exec(gui -> {
|
||||||
|
gui.setHeader("Found " + ex.getBreaks().size() + " problems");
|
||||||
|
gui.addTab("Info", new BreakingDepPage(ex));
|
||||||
|
addReport(gui, reportPath);
|
||||||
|
}, keepRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void execReport(Path reportPath, boolean keepRunning) {
|
||||||
|
exec(gui -> {
|
||||||
|
gui.setHeader("Caught Fatal Error during game startup");
|
||||||
|
addReport(gui, reportPath);
|
||||||
|
}, keepRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addReport(LoaderGui gui, Path reportPath) {
|
||||||
|
gui.addAction("Open Report", event -> PlatformUtil.open(reportPath.toUri()));
|
||||||
|
gui.addAction("Copy Report", event -> PlatformUtil.copyStringContent(reportPath));
|
||||||
|
gui.addTab("Report", new ReportPage(reportPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void exec(Consumer<LoaderGui> init, boolean keepRunning) {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
|
||||||
|
// calling countDown just once will resume execution on this thread
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
LoaderGui gui = new LoaderGui();
|
||||||
|
|
||||||
|
init.accept(gui);
|
||||||
|
|
||||||
|
gui.addWindowListener(new WindowAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void windowClosed(WindowEvent e) {
|
||||||
URLUtil.open(report.toUri());
|
latch.countDown();
|
||||||
}
|
|
||||||
}));
|
|
||||||
actions.add(new JButton(new AbstractAction("Copy Report") {
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e) {
|
|
||||||
URLUtil.copyStringContent(report.toUri());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
actions.add(new JButton(new AbstractAction("Join Discord") {
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e) {
|
|
||||||
URLUtil.open(URI.create("https://discord.frogmc.dev"));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
actions.add(new JButton(new AbstractAction("Exit") {
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e) {
|
|
||||||
frame.dispose();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
frame.add(actions, BorderLayout.SOUTH);
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String printVersionRange(ModDependencyResolver.VersionRange range) {
|
|
||||||
return "range: \n" + range.toString().replace("||", "or");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JPanel getEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon, List<JButton> actions) {
|
|
||||||
JPanel entry = new JPanel(new BorderLayout());
|
|
||||||
entry.setBorder(BasicBorders.getInternalFrameBorder());
|
|
||||||
Box text = Box.createVerticalBox();
|
|
||||||
text.setBorder(BorderFactory.createEmptyBorder());
|
|
||||||
JTextPane desc = new JTextPane();
|
|
||||||
desc.setContentType("text/html");
|
|
||||||
desc.setEditable(false);
|
|
||||||
desc.setBackground(background);
|
|
||||||
desc.setText("<html>" + description.replace("<", "<").replace("\n", "<br>") + "</html>");
|
|
||||||
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
|
||||||
actions.forEach(actionPanel::add);
|
|
||||||
if (icon != null) {
|
|
||||||
URL location = LoaderGui.class.getResource(icon);
|
|
||||||
if (location != null) {
|
|
||||||
entry.add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text.add(desc);
|
|
||||||
JCheckBox showAdvanced = new JCheckBox("Show raw version range");
|
|
||||||
JTextPane advanced = new JTextPane();
|
|
||||||
advanced.setEditable(false);
|
|
||||||
advanced.setBackground(background);
|
|
||||||
advanced.setText(range.toString());
|
|
||||||
advanced.setVisible(false);
|
|
||||||
text.add(advanced);
|
|
||||||
showAdvanced.addActionListener(new AbstractAction() {
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e) {
|
|
||||||
advanced.setVisible(showAdvanced.isSelected());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
entry.add(text);
|
|
||||||
Box bottom = Box.createHorizontalBox();
|
gui.setVisible(true);
|
||||||
bottom.add(showAdvanced, actionPanel);
|
});
|
||||||
entry.add(bottom, BorderLayout.SOUTH);
|
|
||||||
return entry;
|
latch.await();
|
||||||
|
} catch (Throwable error) {
|
||||||
|
LOGGER.warn("Error displaying GUI", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("BusyWait")
|
if (!keepRunning)
|
||||||
public void show() {
|
|
||||||
frame.setVisible(true);
|
|
||||||
|
|
||||||
while (frame.isVisible()) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keepRunning) {
|
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static class Builder {
|
|
||||||
|
|
||||||
private Path report;
|
|
||||||
private Consumer<JFrame> contentFunc;
|
|
||||||
private boolean keepRunning = false;
|
|
||||||
|
|
||||||
public <T> Builder setContent(ContentType<T> type, T argument) {
|
|
||||||
contentFunc = f -> type.contentSetter.accept(f, argument);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder addReport(Path report) {
|
|
||||||
this.report = report;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder keepRunning() {
|
|
||||||
keepRunning = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoaderGui build() {
|
|
||||||
JFrame frame = getFrame(report);
|
|
||||||
if (contentFunc != null) {
|
|
||||||
contentFunc.accept(frame);
|
|
||||||
}
|
|
||||||
return new LoaderGui(frame, keepRunning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class ContentType<T> {
|
|
||||||
public static final ContentType<ModDependencyResolver.UnfulfilledDependencyException> INFO_UNFULFILLED_DEP = new ContentType<>((frame, ex) -> {
|
|
||||||
JPanel pane = new JPanel(new BorderLayout());
|
|
||||||
JTextPane title = new JTextPane();
|
|
||||||
title.setBackground(pane.getBackground());
|
|
||||||
title.setEditable(false);
|
|
||||||
int size = ex.getDependencies().size();
|
|
||||||
title.setText("Found " + size + " Error" + (size > 1 ? "s:" : ":"));
|
|
||||||
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
|
||||||
title.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 0));
|
|
||||||
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
|
||||||
pane.add(title, BorderLayout.NORTH);
|
|
||||||
|
|
||||||
Box list = Box.createVerticalBox();
|
|
||||||
ex.getDependencies().forEach(e -> {
|
|
||||||
StringBuilder description = new StringBuilder();
|
|
||||||
if (e.presentVersion() != null) {
|
|
||||||
description.append("Mod ").append(e.source().id()).append(" (").append(e.source().name()).append(") depends on ");
|
|
||||||
if (e.dependencyName() != null) {
|
|
||||||
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
|
||||||
} else {
|
|
||||||
description.append("a Mod with id ").append(e.dependency());
|
|
||||||
}
|
|
||||||
description.append(" with a version matching ").append(printVersionRange(e.range())).append(", but a different version is present or provided: ").append(e.presentVersion());
|
|
||||||
} else {
|
|
||||||
description.append("Mod ").append(e.source().id()).append(" (").append(e.source().name()).append(") depends on ");
|
|
||||||
if (e.dependencyName() != null) {
|
|
||||||
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
|
||||||
} else {
|
|
||||||
description.append("a Mod with id ").append(e.dependency());
|
|
||||||
}
|
|
||||||
description.append(" with a version matching ").append(printVersionRange(e.range())).append(". \nNo version is currently available.");
|
|
||||||
}
|
|
||||||
description.append("\nSuggested Solution: Install ")
|
|
||||||
.append(e.range().maxCompatible().or(e.range()::minCompatible).map(Objects::toString)
|
|
||||||
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s).orElse("<unknown>"))
|
|
||||||
.append(" of ");
|
|
||||||
if (e.dependencyName() != null) {
|
|
||||||
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
|
||||||
} else {
|
|
||||||
description.append("Mod with id ").append(e.dependency());
|
|
||||||
}
|
|
||||||
List<JButton> actions = new ArrayList<>();
|
|
||||||
if (e.link() != null) {
|
|
||||||
boolean install = e.link().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION);
|
|
||||||
String name = install ? "Install" : "Open mod page";
|
|
||||||
JButton urlButton = new JButton(new AbstractAction(name) {
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent event) {
|
|
||||||
if (install) {
|
|
||||||
ModUtil.installMod(e.link());
|
|
||||||
} else {
|
|
||||||
URLUtil.open(URI.create(e.link()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
actions.add(urlButton);
|
|
||||||
}
|
|
||||||
JPanel entry = getEntry(description.toString(), e.range(), list.getBackground(), e.source().icon(), actions);
|
|
||||||
list.add(entry);
|
|
||||||
});
|
|
||||||
pane.add(new JScrollPane(list));
|
|
||||||
frame.add(pane);
|
|
||||||
});
|
|
||||||
public static final ContentType<ModDependencyResolver.BreakingModException> INFO_BREAKING_DEP = new ContentType<>((frame, ex) -> {
|
|
||||||
JPanel pane = new JPanel(new BorderLayout());
|
|
||||||
JTextPane title = new JTextPane();
|
|
||||||
title.setBackground(pane.getBackground());
|
|
||||||
title.setEditable(false);
|
|
||||||
int size = ex.getBreaks().size();
|
|
||||||
title.setText("Found " + size + " Error" + (size > 1 ? "s:" : ":"));
|
|
||||||
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
|
||||||
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
|
||||||
pane.add(title, BorderLayout.NORTH);
|
|
||||||
|
|
||||||
Box list = Box.createVerticalBox();
|
|
||||||
ex.getBreaks().forEach(e -> {
|
|
||||||
String description = "Mod %s (%s) breaks with mod %s (%s) for versions matching %s \n(present: %s)".formatted(e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), printVersionRange(e.range()), e.broken().version()) +
|
|
||||||
"\nSuggested Solution: Install " +
|
|
||||||
e.range().maxCompatible().or(e.range()::minCompatible).map(Objects::toString)
|
|
||||||
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s).orElse("<unknown>") +
|
|
||||||
" of Mod %s (%s) ".formatted(e.broken().id(), e.broken().name());
|
|
||||||
List<JButton> actions = new ArrayList<>();
|
|
||||||
|
|
||||||
JPanel entry = getEntry(description, e.range(), list.getBackground(), e.source().icon(), actions);
|
|
||||||
entry.setSize(list.getWidth(), entry.getHeight());
|
|
||||||
list.add(entry);
|
|
||||||
});
|
|
||||||
pane.add(new JScrollPane(list));
|
|
||||||
frame.add(pane);
|
|
||||||
});
|
|
||||||
public static final ContentType<Throwable> GENERIC_ERROR = new ContentType<>((frame, throwable) -> {
|
|
||||||
JPanel pane = new JPanel(new BorderLayout());
|
|
||||||
|
|
||||||
JTextPane title = new JTextPane();
|
|
||||||
title.setBackground(pane.getBackground());
|
|
||||||
title.setEditable(false);
|
|
||||||
title.setText("Caught Fatal Error during game startup:");
|
|
||||||
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
|
||||||
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
|
||||||
pane.add(title, BorderLayout.NORTH);
|
|
||||||
|
|
||||||
JTextPane error = new JTextPane();
|
|
||||||
error.setEditable(false);
|
|
||||||
StringWriter writer = new StringWriter();
|
|
||||||
throwable.printStackTrace(new PrintWriter(writer));
|
|
||||||
error.setText(writer.toString());
|
|
||||||
|
|
||||||
pane.add(new JScrollPane(error));
|
|
||||||
|
|
||||||
frame.add(pane);
|
|
||||||
});
|
|
||||||
|
|
||||||
private final BiConsumer<JFrame, T> contentSetter;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
package dev.frogmc.frogloader.impl.gui.component;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.plaf.basic.BasicBorders;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
public class DependencyErrorEntry extends JPanel {
|
||||||
|
|
||||||
|
private final JPanel actions;
|
||||||
|
|
||||||
|
public DependencyErrorEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
|
||||||
|
setBorder(BasicBorders.getInternalFrameBorder());
|
||||||
|
|
||||||
|
Box text = Box.createVerticalBox();
|
||||||
|
text.setBorder(BorderFactory.createEmptyBorder());
|
||||||
|
|
||||||
|
JTextPane desc = new JTextPane();
|
||||||
|
desc.setContentType("text/html");
|
||||||
|
desc.setEditable(false);
|
||||||
|
desc.setBackground(background);
|
||||||
|
desc.setText("<html>" + description.replace("<", "<").replace("\n", "<br>") + "</html>");
|
||||||
|
text.add(desc);
|
||||||
|
|
||||||
|
add(text, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
this.actions = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
|
add(this.actions, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
if (icon != null) {
|
||||||
|
URL location = getClass().getResource(icon);
|
||||||
|
|
||||||
|
if (location != null)
|
||||||
|
add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAction(String label, ActionListener listener) {
|
||||||
|
var button = new JButton(label);
|
||||||
|
button.addActionListener(listener);
|
||||||
|
this.actions.add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package dev.frogmc.frogloader.impl.gui.page;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class BreakingDepPage extends JScrollPane {
|
||||||
|
|
||||||
|
public BreakingDepPage(ModDependencyResolver.BreakingModException ex) {
|
||||||
|
getHorizontalScrollBar().setUnitIncrement(16);
|
||||||
|
getVerticalScrollBar().setUnitIncrement(16);
|
||||||
|
|
||||||
|
Box list = Box.createVerticalBox();
|
||||||
|
|
||||||
|
ex.getBreaks().forEach(entry -> {
|
||||||
|
String description =
|
||||||
|
"""
|
||||||
|
Mod %s (%s) breaks with mod %s (%s) for versions matching range: %s (present: %s)
|
||||||
|
Suggested Solution: Install %s of Mod %s (%s)
|
||||||
|
""";
|
||||||
|
|
||||||
|
description = description.formatted(
|
||||||
|
entry.source().id(),
|
||||||
|
entry.source().name(),
|
||||||
|
entry.broken().id(),
|
||||||
|
entry.broken().name(),
|
||||||
|
entry.range().toString(" or "),
|
||||||
|
entry.broken().version(),
|
||||||
|
entry.range()
|
||||||
|
.maxCompatible()
|
||||||
|
.or(entry.range()::minCompatible)
|
||||||
|
.map(Objects::toString)
|
||||||
|
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
|
||||||
|
.orElse("<unknown>"),
|
||||||
|
entry.broken().id(),
|
||||||
|
entry.broken().name()
|
||||||
|
);
|
||||||
|
|
||||||
|
DependencyErrorEntry result = new DependencyErrorEntry(
|
||||||
|
description,
|
||||||
|
entry.range(),
|
||||||
|
list.getBackground(),
|
||||||
|
entry.source().icon()
|
||||||
|
);
|
||||||
|
list.add(result);
|
||||||
|
});
|
||||||
|
setViewportView(list);
|
||||||
|
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package dev.frogmc.frogloader.impl.gui.page;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class ReportPage extends JScrollPane {
|
||||||
|
|
||||||
|
public ReportPage(Path reportPath) {
|
||||||
|
getHorizontalScrollBar().setUnitIncrement(16);
|
||||||
|
getVerticalScrollBar().setUnitIncrement(16);
|
||||||
|
|
||||||
|
JTextPane text = new JTextPane();
|
||||||
|
text.setEditable(false);
|
||||||
|
try {
|
||||||
|
text.setText(Files.readString(reportPath, StandardCharsets.UTF_8));
|
||||||
|
} catch (IOException e) {
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
PrintWriter printer = new PrintWriter(writer);
|
||||||
|
printer.printf("Could not load contents of %s:%n", reportPath);
|
||||||
|
e.printStackTrace(printer);
|
||||||
|
}
|
||||||
|
setViewportView(text);
|
||||||
|
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point(0, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package dev.frogmc.frogloader.impl.gui.page;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
|
import dev.frogmc.frogloader.impl.util.PlatformUtil;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class UnfulfilledDepPage extends JScrollPane {
|
||||||
|
|
||||||
|
public UnfulfilledDepPage(ModDependencyResolver.UnfulfilledDependencyException ex) {
|
||||||
|
getHorizontalScrollBar().setUnitIncrement(16);
|
||||||
|
getVerticalScrollBar().setUnitIncrement(16);
|
||||||
|
|
||||||
|
Box list = Box.createVerticalBox();
|
||||||
|
|
||||||
|
ex.getDependencies().forEach(entry -> {
|
||||||
|
StringBuilder description = new StringBuilder();
|
||||||
|
if (entry.presentVersion() != null) {
|
||||||
|
description.append("Mod ").append(entry.source().id()).append(" (").append(entry.source().name()).append(") depends on ");
|
||||||
|
if (entry.dependencyName() != null) {
|
||||||
|
description.append(entry.dependency()).append(" (").append(entry.dependencyName()).append(") ");
|
||||||
|
} else {
|
||||||
|
description.append("a Mod with id ").append(entry.dependency());
|
||||||
|
}
|
||||||
|
description.append(" with a version matching range:\n").append(entry.range().toString(" or ")).append("\nbut a different version is present or provided: ").append(entry.presentVersion());
|
||||||
|
} else {
|
||||||
|
description.append("Mod ").append(entry.source().id()).append(" (").append(entry.source().name()).append(") depends on ");
|
||||||
|
if (entry.dependencyName() != null) {
|
||||||
|
description.append(entry.dependency()).append(" (").append(entry.dependencyName()).append(") ");
|
||||||
|
} else {
|
||||||
|
description.append("a Mod with id ").append(entry.dependency());
|
||||||
|
}
|
||||||
|
description.append(" with a version matching range:\n").append(entry.range().toString(" or ")).append("\nNo version is currently available.");
|
||||||
|
}
|
||||||
|
description.append("\nSuggested Solution: Install ")
|
||||||
|
.append(
|
||||||
|
entry
|
||||||
|
.range()
|
||||||
|
.maxCompatible()
|
||||||
|
.or(entry.range()::minCompatible)
|
||||||
|
.map(Objects::toString)
|
||||||
|
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
|
||||||
|
.orElse("<unknown>")
|
||||||
|
)
|
||||||
|
.append(" of ");
|
||||||
|
if (entry.dependencyName() != null) {
|
||||||
|
description.append(entry.dependency()).append(" (").append(entry.dependencyName()).append(") ");
|
||||||
|
} else {
|
||||||
|
description.append("Mod with id ").append(entry.dependency());
|
||||||
|
}
|
||||||
|
|
||||||
|
DependencyErrorEntry result = new DependencyErrorEntry(
|
||||||
|
description.toString(),
|
||||||
|
entry.range(),
|
||||||
|
list.getBackground(),
|
||||||
|
entry.source().icon()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entry.link() != null) {
|
||||||
|
try {
|
||||||
|
URI uri = new URI(entry.link());
|
||||||
|
result.addAction("Open mod page", event -> PlatformUtil.open(uri));
|
||||||
|
} catch (URISyntaxException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.add(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
setViewportView(list);
|
||||||
|
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -153,7 +153,7 @@ public class AccessWidener {
|
||||||
public record Entry(AccessType type, String targetType, String className, String name, String descriptor) {
|
public record Entry(AccessType type, String targetType, String className, String name, String descriptor) {
|
||||||
|
|
||||||
public Entry(String[] line) {
|
public Entry(String[] line) {
|
||||||
this(AccessType.of(line[0]), line[1], line[2], line[3], line[4]);
|
this(AccessType.of(line[0]), line[1], line[2], line.length > 3 ? line[3] : null, line.length > 4 ? line[4] : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAccessGreaterThan(Entry other) {
|
public boolean isAccessGreaterThan(Entry other) {
|
||||||
|
|
|
@ -557,7 +557,11 @@ public class ModDependencyResolver {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return sets.stream().map(Objects::toString).collect(Collectors.joining(" || "));
|
return toString(" || ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString(String delimeter) {
|
||||||
|
return sets.stream().map(Objects::toString).collect(Collectors.joining(delimeter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<SemVer> maxCompatible() {
|
public Optional<SemVer> maxCompatible() {
|
||||||
|
|
|
@ -83,9 +83,9 @@ public class Minecraft implements FrogPlugin {
|
||||||
try {
|
try {
|
||||||
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
|
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
|
||||||
} catch (ModDependencyResolver.BreakingModException e) {
|
} catch (ModDependencyResolver.BreakingModException e) {
|
||||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
LoaderGui.execBreakingDep(CrashReportGenerator.writeReport(e, modProperties), e, false);
|
||||||
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
||||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
LoaderGui.execUnfulfilledDep(CrashReportGenerator.writeReport(e, modProperties), e, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> {
|
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> {
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
package dev.frogmc.frogloader.impl.util;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.datatransfer.StringSelection;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class PlatformUtil {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(PlatformUtil.class);
|
||||||
|
|
||||||
|
public static void open(URI uri) {
|
||||||
|
LOGGER.info("Opening: {}", uri);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
|
Desktop.getDesktop().browse(uri);
|
||||||
|
} else {
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("xdg-open", uri.toString());
|
||||||
|
builder.start();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to open url: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void copyStringContent(Path path) {
|
||||||
|
try {
|
||||||
|
if (System.getenv()
|
||||||
|
.getOrDefault("DESKTOP_SESSION", "")
|
||||||
|
.toLowerCase(Locale.ROOT)
|
||||||
|
.contains("wayland")) {
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy < " + path);
|
||||||
|
builder.start();
|
||||||
|
} else {
|
||||||
|
String data = Files.readString(path, StandardCharsets.UTF_8);
|
||||||
|
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to copy contents of {}:", path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
package dev.frogmc.frogloader.impl.util;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.awt.datatransfer.StringSelection;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class URLUtil {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(URLUtil.class);
|
|
||||||
|
|
||||||
public static void open(URI url) {
|
|
||||||
|
|
||||||
LOGGER.info("Opening: {}", url);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
|
||||||
Desktop.getDesktop().browse(url);
|
|
||||||
} else {
|
|
||||||
ProcessBuilder builder = new ProcessBuilder("xdg-open", url.toString());
|
|
||||||
builder.start();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Failed to open url: ", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void copyStringContent(URI url) {
|
|
||||||
if (System.getenv().getOrDefault("DESKTOP_SESSION", "").toLowerCase(Locale.ROOT)
|
|
||||||
.contains("wayland")) {
|
|
||||||
try {
|
|
||||||
ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy < " + url);
|
|
||||||
builder.start();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Failed to copy contents of {}:", url, e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try (InputStream in = url.toURL().openStream();
|
|
||||||
InputStreamReader inReader = new InputStreamReader(in);
|
|
||||||
BufferedReader reader = new BufferedReader(inReader)) {
|
|
||||||
String data = reader.lines().collect(Collectors.joining("\n"));
|
|
||||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Failed to copy contents of {}:", url, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue