From 93afd3a063c802939f176b86ed6d71d40ceebf94 Mon Sep 17 00:00:00 2001 From: ThatGamerBlue Date: Sun, 1 Mar 2020 05:59:45 +0000 Subject: [PATCH] externalmanager: rework config format, add direct repo support, change icons --- .../client/plugins/ExternalPluginManager.java | 85 +++++++++++-- .../externals/ExternalPluginManagerPanel.java | 113 +++++++++++++++--- .../net/runelite/client/util/MiscUtils.java | 32 ++++- .../openosrs/externals/add_raw_icon.png | Bin 0 -> 18177 bytes .../plugins/openosrs/externals/gh_icon.png | Bin 0 -> 18201 bytes 5 files changed, 198 insertions(+), 32 deletions(-) create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/openosrs/externals/add_raw_icon.png create mode 100644 runelite-client/src/main/resources/net/runelite/client/plugins/openosrs/externals/gh_icon.png diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java index eec6d2bb2e..2565bf6793 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java @@ -50,6 +50,7 @@ import net.runelite.client.eventbus.EventBus; import net.runelite.client.events.ExternalPluginChanged; import net.runelite.client.events.ExternalRepositoryChanged; import net.runelite.client.ui.RuneLiteSplashScreen; +import net.runelite.client.util.MiscUtils; import net.runelite.client.util.SwingUtil; import org.pf4j.DefaultPluginManager; import org.pf4j.DependencyResolver; @@ -184,22 +185,41 @@ class ExternalPluginManager this.externalPluginManager.setSystemVersion(SYSTEM_VERSION); } + public boolean doesGhRepoExist(String owner, String name) + { + return doesRepoExist("gh:" + owner + "/" + name); + } + + /** + * Note that {@link org.pf4j.update.UpdateManager#addRepository} checks if the repo exists, however it throws an exception which is bad + */ + public boolean doesRepoExist(String id) + { + return repositories.stream().anyMatch((repo) -> repo.getId().equals(id)); + } + private static URL toRepositoryUrl(String owner, String name) throws MalformedURLException { return new URL("https://raw.githubusercontent.com/" + owner + "/" + name + "/master/"); } - public static boolean testRepository(String owner, String name) + public static boolean testGHRepository(String owner, String name) { - final List repositories = new ArrayList<>(); try { - repositories.add(new DefaultUpdateRepository("github", new URL("https://raw.githubusercontent.com/" + owner + "/" + name + "/master/"))); + return testRepository(toRepositoryUrl(owner, name)); } catch (MalformedURLException e) { - return true; + e.printStackTrace(); } + return false; + } + + public static boolean testRepository(URL url) + { + final List repositories = new ArrayList<>(); + repositories.add(new DefaultUpdateRepository("repository-testing", url)); DefaultPluginManager testPluginManager = new DefaultPluginManager(EXTERNALPLUGIN_DIR.toPath()); UpdateManager updateManager = new UpdateManager(testPluginManager, repositories); @@ -238,6 +258,42 @@ class ExternalPluginManager } public void startExternalUpdateManager() + { + if (!tryLoadNewFormat()) + { + loadOldFormat(); + } + + this.updateManager = new UpdateManager(this.externalPluginManager, repositories); + } + + public boolean tryLoadNewFormat() + { + try + { + for (String keyval : openOSRSConfig.getExternalRepositories().split(";")) + { + String[] split = keyval.split("\\|"); + if (split.length != 2) + { + repositories.clear(); + return false; + } + String id = split[0]; + String url = split[1]; + + repositories.add(new DefaultUpdateRepository(id, new URL(url))); + } + } + catch (ArrayIndexOutOfBoundsException | MalformedURLException e) + { + repositories.clear(); + return false; + } + return true; + } + + public void loadOldFormat() { try { @@ -253,18 +309,13 @@ class ExternalPluginManager { e.printStackTrace(); } - - this.updateManager = new UpdateManager(this.externalPluginManager, repositories); } - public void addRepository(String owner, String name) + public void addGHRepository(String owner, String name) { try { - DefaultUpdateRepository respository = new DefaultUpdateRepository(owner + toRepositoryUrl(owner, name), toRepositoryUrl(owner, name)); - updateManager.addRepository(respository); - eventBus.post(ExternalRepositoryChanged.class, new ExternalRepositoryChanged(owner + toRepositoryUrl(owner, name), true)); - saveConfig(); + addRepository("gh:" + owner + "/" + name, toRepositoryUrl(owner, name)); } catch (MalformedURLException e) { @@ -272,6 +323,14 @@ class ExternalPluginManager } } + public void addRepository(String key, URL url) + { + DefaultUpdateRepository respository = new DefaultUpdateRepository(key, url); + updateManager.addRepository(respository); + eventBus.post(ExternalRepositoryChanged.class, new ExternalRepositoryChanged(key, true)); + saveConfig(); + } + public void removeRepository(String owner) { updateManager.removeRepository(owner); @@ -286,8 +345,8 @@ class ExternalPluginManager for (UpdateRepository repository : updateManager.getRepositories()) { config.append(repository.getId()); - config.append(":"); - config.append(repository.getUrl().toString()); + config.append("|"); + config.append(MiscUtils.urlToStringEncoded(repository.getUrl())); config.append(";"); } config.deleteCharAt(config.lastIndexOf(";")); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/ExternalPluginManagerPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/ExternalPluginManagerPanel.java index e01e2aa509..61e8e13e9f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/ExternalPluginManagerPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/externals/ExternalPluginManagerPanel.java @@ -6,7 +6,12 @@ import java.awt.Dimension; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.concurrent.ScheduledExecutorService; +import javax.imageio.ImageIO; import javax.inject.Inject; import javax.swing.ImageIcon; import javax.swing.JLabel; @@ -16,25 +21,29 @@ import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.JTextField; import javax.swing.border.EmptyBorder; +import lombok.SneakyThrows; import net.runelite.client.eventbus.EventBus; import net.runelite.client.plugins.ExternalPluginManager; +import net.runelite.client.plugins.info.InfoPlugin; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.PluginPanel; import net.runelite.client.util.ImageUtil; public class ExternalPluginManagerPanel extends PluginPanel { - private static final ImageIcon ADD_ICON; - private static final ImageIcon ADD_HOVER_ICON; + private static final ImageIcon ADD_ICON_RAW; + private static final ImageIcon ADD_HOVER_ICON_RAW; + private static final ImageIcon ADD_ICON_GH; + private static final ImageIcon ADD_HOVER_ICON_GH; static { - final BufferedImage addIcon = - ImageUtil.recolorImage( - ImageUtil.getResourceStreamFromClass(ExternalPluginManagerPanel.class, "add_icon.png"), ColorScheme.BRAND_BLUE - ); - ADD_ICON = new ImageIcon(addIcon); - ADD_HOVER_ICON = new ImageIcon(ImageUtil.alphaOffset(addIcon, 0.53f)); + final BufferedImage addIconRaw = ImageUtil.getResourceStreamFromClass(ExternalPluginManagerPanel.class, "add_raw_icon.png"); + final BufferedImage addIconGh = ImageUtil.resizeImage(ImageUtil.getResourceStreamFromClass(ExternalPluginManagerPanel.class, "gh_icon.png"), 14, 14); + ADD_ICON_RAW = new ImageIcon(addIconRaw); + ADD_HOVER_ICON_RAW = new ImageIcon(ImageUtil.alphaOffset(addIconRaw, 0.53f)); + ADD_ICON_GH = new ImageIcon(addIconGh); + ADD_HOVER_ICON_GH = new ImageIcon(ImageUtil.alphaOffset(addIconGh, 0.53f)); } private final ExternalPluginManager externalPluginManager; @@ -73,13 +82,17 @@ public class ExternalPluginManagerPanel extends PluginPanel titlePanel.setBorder(new EmptyBorder(10, 10, 10, 10)); JLabel title = new JLabel(); - JLabel addRepo = new JLabel(ADD_ICON); + JLabel addGHRepo = new JLabel(ADD_ICON_GH); + JLabel addRawRepo = new JLabel(ADD_ICON_RAW); + + JPanel buttonHolder = new JPanel(new BorderLayout()); + buttonHolder.setBorder(new EmptyBorder(0, 0, 0, 0)); title.setText("External Plugin Manager"); title.setForeground(Color.WHITE); - addRepo.setToolTipText("Add new repository"); - addRepo.addMouseListener(new MouseAdapter() + addGHRepo.setToolTipText("Add new GitHub repository"); + addGHRepo.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent mouseEvent) @@ -97,30 +110,98 @@ public class ExternalPluginManagerPanel extends PluginPanel return; } - if (ExternalPluginManager.testRepository(owner.getText(), name.getText())) + if (externalPluginManager.doesGhRepoExist(owner.getText(), name.getText())) + { + JOptionPane.showMessageDialog(null, "This repository already exists.", "Error!", JOptionPane.ERROR_MESSAGE); + return; + } + + if (ExternalPluginManager.testGHRepository(owner.getText(), name.getText())) { JOptionPane.showMessageDialog(null, "This doesn't appear to be a valid repository.", "Error!", JOptionPane.ERROR_MESSAGE); return; } - externalPluginManager.addRepository(owner.getText(), name.getText()); + externalPluginManager.addGHRepository(owner.getText(), name.getText()); } @Override public void mouseEntered(MouseEvent mouseEvent) { - addRepo.setIcon(ADD_HOVER_ICON); + addGHRepo.setIcon(ADD_HOVER_ICON_GH); } @Override public void mouseExited(MouseEvent mouseEvent) { - addRepo.setIcon(ADD_ICON); + addGHRepo.setIcon(ADD_ICON_GH); } }); + addGHRepo.setBorder(new EmptyBorder(0, 3, 0, 0)); + + addRawRepo.setToolTipText("Add new raw repository"); + addRawRepo.addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent mouseEvent) + { + JTextField id = new JTextField(); + JTextField url = new JTextField(); + Object[] message = { + "Repository ID:", id, + "Repository URL:", url + }; + + int option = JOptionPane.showConfirmDialog(null, message, "Add repository", JOptionPane.OK_CANCEL_OPTION); + if (option != JOptionPane.OK_OPTION || id.getText().equals("") || url.getText().equals("")) + { + return; + } + + if (externalPluginManager.doesRepoExist(id.getText())) + { + JOptionPane.showMessageDialog(null, String.format("The repository with id %s already exists.", id.getText()), "Error!", JOptionPane.ERROR_MESSAGE); + return; + } + + URL urlActual; + try + { + urlActual = new URL(url.getText()); + } + catch (MalformedURLException e) + { + JOptionPane.showMessageDialog(null, "This doesn't appear to be a valid repository.", "Error!", JOptionPane.ERROR_MESSAGE); + return; + } + + if (ExternalPluginManager.testRepository(urlActual)) + { + JOptionPane.showMessageDialog(null, "This doesn't appear to be a valid repository.", "Error!", JOptionPane.ERROR_MESSAGE); + return; + } + + externalPluginManager.addRepository(id.getText(), urlActual); + } + + @Override + public void mouseEntered(MouseEvent mouseEvent) + { + addRawRepo.setIcon(ADD_HOVER_ICON_RAW); + } + + @Override + public void mouseExited(MouseEvent mouseEvent) + { + addRawRepo.setIcon(ADD_ICON_RAW); + } + }); + addRawRepo.setBorder(new EmptyBorder(0, 0, 0, 3)); titlePanel.add(title, BorderLayout.WEST); - titlePanel.add(addRepo, BorderLayout.EAST); + buttonHolder.add(addRawRepo, BorderLayout.WEST); + buttonHolder.add(addGHRepo, BorderLayout.EAST); + titlePanel.add(buttonHolder, BorderLayout.EAST); return titlePanel; } diff --git a/runelite-client/src/main/java/net/runelite/client/util/MiscUtils.java b/runelite-client/src/main/java/net/runelite/client/util/MiscUtils.java index 29e7d7b93f..832efb0635 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/MiscUtils.java +++ b/runelite-client/src/main/java/net/runelite/client/util/MiscUtils.java @@ -1,12 +1,15 @@ package net.runelite.client.util; -import java.awt.Polygon; -import java.time.Duration; -import java.time.temporal.ChronoUnit; import net.runelite.api.Client; import net.runelite.api.Player; import net.runelite.api.WorldType; import net.runelite.api.coords.WorldPoint; +import java.awt.Polygon; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; public class MiscUtils { @@ -170,4 +173,27 @@ public class MiscUtils } return str; } + + /** + * Mostly stolen from {@link java.net.URLStreamHandler#toExternalForm(URL)} + * + * @param url URL to encode + * @return URL, with path, query and ref encoded + */ + public static String urlToStringEncoded(URL url) + { + String s; + return url.getProtocol() + + ':' + + (((s = url.getAuthority()) != null && s.length() > 0) + ? "//" + s : "") + + (((s = url.getPath()) != null) ? urlEncode(s) : "") + + (((s = url.getQuery()) != null) ? '?' + urlEncode(s) : "") + + (((s = url.getRef()) != null) ? '#' + urlEncode(s) : ""); + } + + private static String urlEncode(String s) + { + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } } diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/openosrs/externals/add_raw_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/openosrs/externals/add_raw_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3d833268c0b94f6b46dc9c4bb2137fd60afae1ec GIT binary patch literal 18177 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 literal 0 HcmV?d00001 diff --git a/runelite-client/src/main/resources/net/runelite/client/plugins/openosrs/externals/gh_icon.png b/runelite-client/src/main/resources/net/runelite/client/plugins/openosrs/externals/gh_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7c5da1b5b8a18486f49c26360e4a6f4dcba3e874 GIT binary patch literal 18201 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 Nu(x)$DzNn3_FsbX5PSdt literal 0 HcmV?d00001