Refactor + a few improvements to UI #8

Merged
Ecorous merged 5 commits from TheKodeToad/rework-ui into main 2024-06-12 13:37:25 -04:00
10 changed files with 381 additions and 321 deletions
Showing only changes of commit 0a11d66ce8 - Show all commits

View file

@ -79,7 +79,7 @@ public class FrogLoaderImpl implements FrogLoader {
LOGGER.info("Launching...");
plugins.forEach(FrogPlugin::run);
} 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);
}
}

View file

@ -1,281 +1,136 @@
package dev.frogmc.frogloader.impl.gui;
import javax.swing.*;
import javax.swing.plaf.basic.BasicBorders;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.concurrent.CountDownLatch;
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.ModUtil;
import dev.frogmc.frogloader.impl.util.URLUtil;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.Nullable;
import dev.frogmc.frogloader.impl.util.PlatformUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class LoaderGui {
public class LoaderGui extends JFrame {
private final JFrame frame;
private final boolean keepRunning;
private static final Logger LOGGER = LoggerFactory.getLogger(LoaderGui.class);
public static Builder builder() {
return new Builder();
private final JLabel header;
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) {
var frame = new JFrame();
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
frame.setSize(952, 560);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
public void setHeader(String text) {
this.header.setText(text);
}
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT));
if (report != null) {
actions.add(new JButton(new AbstractAction("Open Report") {
@Override
public void actionPerformed(ActionEvent e) {
URLUtil.open(report.toUri());
}
}));
actions.add(new JButton(new AbstractAction("Copy Report") {
@Override
public void actionPerformed(ActionEvent e) {
URLUtil.copyStringContent(report.toUri());
}
}));
public void addTab(String name, Component component) {
this.tabbedPane.add(name, component);
}
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
public void windowClosed(WindowEvent e) {
latch.countDown();
}
});
gui.setVisible(true);
});
latch.await();
} catch (Throwable error) {
LOGGER.warn("Error displaying GUI", error);
}
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("<", "&lt;").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();
bottom.add(showAdvanced, actionPanel);
entry.add(bottom, BorderLayout.SOUTH);
return entry;
}
@SuppressWarnings("BusyWait")
public void show() {
frame.setVisible(true);
while (frame.isVisible()) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
if (!keepRunning) {
if (!keepRunning)
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(error);
frame.add(pane);
});
private final BiConsumer<JFrame, T> contentSetter;
}
}

View file

@ -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("<", "&lt;").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);
}
}

View file

@ -0,0 +1,52 @@
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.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);
}
}

View file

@ -0,0 +1,27 @@
package dev.frogmc.frogloader.impl.gui.page;
import dev.frogmc.frogloader.impl.util.PlatformUtil;
import javax.swing.*;
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 JTextPane {
public ReportPage(Path reportPath) {
setEditable(false);
try {
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);
}
}
}

View file

@ -0,0 +1,78 @@
package dev.frogmc.frogloader.impl.gui.page;
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
import dev.frogmc.frogloader.impl.mod.ModUtil;
import dev.frogmc.frogloader.impl.util.PlatformUtil;
import javax.swing.*;
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);
}
}

View file

@ -557,7 +557,11 @@ public class ModDependencyResolver {
@Override
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() {

View file

@ -83,9 +83,9 @@ public class Minecraft implements FrogPlugin {
try {
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
} 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) {
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 -> {

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}