runelite-client: Add External Plugin support
This commit is contained in:
@@ -57,6 +57,7 @@ import net.runelite.client.game.ItemManager;
|
||||
import net.runelite.client.game.LootManager;
|
||||
import net.runelite.client.game.chatbox.ChatboxPanelManager;
|
||||
import net.runelite.client.menus.MenuManager;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManager;
|
||||
import net.runelite.client.plugins.PluginManager;
|
||||
import net.runelite.client.rs.ClientLoader;
|
||||
import net.runelite.client.rs.ClientUpdateCheckMode;
|
||||
@@ -80,6 +81,7 @@ public class RuneLite
|
||||
{
|
||||
public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite");
|
||||
public static final File CACHE_DIR = new File(RUNELITE_DIR, "cache");
|
||||
public static final File PLUGINS_DIR = new File(RUNELITE_DIR, "plugins");
|
||||
public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles");
|
||||
public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots");
|
||||
public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs");
|
||||
@@ -90,6 +92,9 @@ public class RuneLite
|
||||
@Inject
|
||||
private PluginManager pluginManager;
|
||||
|
||||
@Inject
|
||||
private ExternalPluginManager externalPluginManager;
|
||||
|
||||
@Inject
|
||||
private EventBus eventBus;
|
||||
|
||||
@@ -288,12 +293,13 @@ public class RuneLite
|
||||
// Load the plugins, but does not start them yet.
|
||||
// This will initialize configuration
|
||||
pluginManager.loadCorePlugins();
|
||||
externalPluginManager.loadExternalPlugins();
|
||||
|
||||
SplashScreen.stage(.70, null, "Finalizing configuration");
|
||||
|
||||
// Plugins have provided their config, so set default config
|
||||
// to main settings
|
||||
pluginManager.loadDefaultPluginConfiguration();
|
||||
pluginManager.loadDefaultPluginConfiguration(null);
|
||||
|
||||
// Start client session
|
||||
clientSessionManager.start();
|
||||
@@ -309,6 +315,7 @@ public class RuneLite
|
||||
// Register event listeners
|
||||
eventBus.register(clientUI);
|
||||
eventBus.register(pluginManager);
|
||||
eventBus.register(externalPluginManager);
|
||||
eventBus.register(overlayManager);
|
||||
eventBus.register(drawManager);
|
||||
eventBus.register(infoBoxManager);
|
||||
@@ -337,7 +344,7 @@ public class RuneLite
|
||||
}
|
||||
|
||||
// Start plugins
|
||||
pluginManager.startCorePlugins();
|
||||
pluginManager.startPlugins();
|
||||
|
||||
SplashScreen.stop();
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Properties;
|
||||
import javax.annotation.Nullable;
|
||||
import okhttp3.HttpUrl;
|
||||
|
||||
public class RuneLiteProperties
|
||||
{
|
||||
@@ -45,6 +46,8 @@ public class RuneLiteProperties
|
||||
private static final String DNS_CHANGE_LINK = "runelite.dnschange.link";
|
||||
private static final String JAV_CONFIG = "runelite.jav_config";
|
||||
private static final String JAV_CONFIG_BACKUP = "runelite.jav_config_backup";
|
||||
private static final String PLUGINHUB_BASE = "runelite.pluginhub.url";
|
||||
private static final String PLUGINHUB_VERSION = "runelite.pluginhub.version";
|
||||
|
||||
private static final Properties properties = new Properties();
|
||||
|
||||
@@ -130,4 +133,10 @@ public class RuneLiteProperties
|
||||
{
|
||||
return properties.getProperty(JAV_CONFIG_BACKUP);
|
||||
}
|
||||
|
||||
public static HttpUrl getPluginHubBase()
|
||||
{
|
||||
String version = System.getProperty(PLUGINHUB_VERSION, properties.getProperty(PLUGINHUB_VERSION));
|
||||
return HttpUrl.parse(properties.get(PLUGINHUB_BASE) + "/" + version);
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,11 @@ import java.awt.Dimension;
|
||||
import net.runelite.api.Constants;
|
||||
import net.runelite.client.ui.ContainableFrame;
|
||||
|
||||
@ConfigGroup("runelite")
|
||||
@ConfigGroup(RuneLiteConfig.GROUP_NAME)
|
||||
public interface RuneLiteConfig extends Config
|
||||
{
|
||||
String GROUP_NAME = "runelite";
|
||||
|
||||
@ConfigItem(
|
||||
keyName = "gameSize",
|
||||
name = "Game size",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2019 Abex
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package net.runelite.client.events;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Value;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManifest;
|
||||
|
||||
/**
|
||||
* Posted when an external plugin has been added, removed, or updated
|
||||
*/
|
||||
@Value
|
||||
public class ExternalPluginsChanged
|
||||
{
|
||||
private final List<ExternalPluginManifest> loadedManifest;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2019 Abex
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package net.runelite.client.externalplugins;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import lombok.Getter;
|
||||
|
||||
class ExternalPluginClassLoader extends URLClassLoader
|
||||
{
|
||||
@Getter
|
||||
private final ExternalPluginManifest manifest;
|
||||
|
||||
ExternalPluginClassLoader(ExternalPluginManifest manifest, URL[] urls)
|
||||
{
|
||||
super(urls, ExternalPluginClassLoader.class.getClassLoader());
|
||||
this.manifest = manifest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2019 Abex
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package net.runelite.client.externalplugins;
|
||||
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.util.List;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.inject.Inject;
|
||||
import net.runelite.client.RuneLiteProperties;
|
||||
import net.runelite.http.api.RuneLiteAPI;
|
||||
import net.runelite.client.util.VerificationException;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okio.BufferedSource;
|
||||
|
||||
public class ExternalPluginClient
|
||||
{
|
||||
private final OkHttpClient cachingClient;
|
||||
|
||||
@Inject
|
||||
public ExternalPluginClient(OkHttpClient cachingClient)
|
||||
{
|
||||
this.cachingClient = cachingClient;
|
||||
}
|
||||
|
||||
public List<ExternalPluginManifest> downloadManifest() throws IOException, VerificationException
|
||||
{
|
||||
HttpUrl manifest = RuneLiteProperties.getPluginHubBase()
|
||||
.newBuilder()
|
||||
.addPathSegments("manifest.js")
|
||||
.build();
|
||||
try (Response res = cachingClient.newCall(new Request.Builder().url(manifest).build()).execute())
|
||||
{
|
||||
if (res.code() != 200)
|
||||
{
|
||||
throw new IOException("Non-OK response code: " + res.code());
|
||||
}
|
||||
|
||||
BufferedSource src = res.body().source();
|
||||
|
||||
byte[] signature = new byte[src.readInt()];
|
||||
src.readFully(signature);
|
||||
|
||||
byte[] data = src.readByteArray();
|
||||
Signature s = Signature.getInstance("SHA256withRSA");
|
||||
s.initVerify(loadCertificate());
|
||||
s.update(data);
|
||||
|
||||
if (!s.verify(signature))
|
||||
{
|
||||
throw new VerificationException("Unable to verify external plugin manifest");
|
||||
}
|
||||
|
||||
return RuneLiteAPI.GSON.fromJson(new String(data, StandardCharsets.UTF_8),
|
||||
new TypeToken<List<ExternalPluginManifest>>()
|
||||
{
|
||||
}.getType());
|
||||
}
|
||||
catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public BufferedImage downloadIcon(ExternalPluginManifest plugin) throws IOException
|
||||
{
|
||||
if (!plugin.hasIcon())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpUrl url = RuneLiteProperties.getPluginHubBase()
|
||||
.newBuilder()
|
||||
.addPathSegment(plugin.getInternalName())
|
||||
.addPathSegment(plugin.getCommit() + ".png")
|
||||
.build();
|
||||
|
||||
try (Response res = cachingClient.newCall(new Request.Builder().url(url).build()).execute())
|
||||
{
|
||||
byte[] bytes = res.body().bytes();
|
||||
// We don't stream so the lock doesn't block the edt trying to load something at the same time
|
||||
synchronized (ImageIO.class)
|
||||
{
|
||||
return ImageIO.read(new ByteArrayInputStream(bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Certificate loadCertificate()
|
||||
{
|
||||
try
|
||||
{
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
Certificate certificate = certFactory.generateCertificate(ExternalPluginClient.class.getResourceAsStream("externalplugins.crt"));
|
||||
return certificate;
|
||||
}
|
||||
catch (CertificateException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/*
|
||||
* Copyright (c) 2019 Abex
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package net.runelite.client.externalplugins;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.hash.HashingInputStream;
|
||||
import com.google.common.io.Files;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Function;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.runelite.client.RuneLite;
|
||||
import net.runelite.client.RuneLiteProperties;
|
||||
import net.runelite.client.config.ConfigManager;
|
||||
import net.runelite.client.config.RuneLiteConfig;
|
||||
import net.runelite.client.eventbus.EventBus;
|
||||
import net.runelite.client.eventbus.Subscribe;
|
||||
import net.runelite.client.events.ExternalPluginsChanged;
|
||||
import net.runelite.client.events.SessionClose;
|
||||
import net.runelite.client.events.SessionOpen;
|
||||
import net.runelite.client.plugins.Plugin;
|
||||
import net.runelite.client.plugins.PluginInstantiationException;
|
||||
import net.runelite.client.plugins.PluginManager;
|
||||
import net.runelite.client.ui.SplashScreen;
|
||||
import net.runelite.client.util.CountingInputStream;
|
||||
import net.runelite.client.util.Text;
|
||||
import net.runelite.client.util.VerificationException;
|
||||
import net.runelite.http.api.RuneLiteAPI;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class ExternalPluginManager
|
||||
{
|
||||
private static final String PLUGIN_LIST_KEY = "externalPlugins";
|
||||
private static Class<? extends Plugin>[] builtinExternals = null;
|
||||
|
||||
@Inject
|
||||
private ConfigManager configManager;
|
||||
|
||||
@Inject
|
||||
private ExternalPluginClient externalPluginClient;
|
||||
|
||||
@Inject
|
||||
private PluginManager pluginManager;
|
||||
|
||||
@Inject
|
||||
private ScheduledExecutorService executor;
|
||||
|
||||
@Inject
|
||||
private EventBus eventBus;
|
||||
|
||||
public void loadExternalPlugins() throws PluginInstantiationException
|
||||
{
|
||||
refreshPlugins();
|
||||
|
||||
if (builtinExternals != null)
|
||||
{
|
||||
// builtin external's don't actually have a manifest or a separate classloader...
|
||||
pluginManager.loadPlugins(Lists.newArrayList(builtinExternals), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSessionOpen(SessionOpen event)
|
||||
{
|
||||
executor.submit(this::refreshPlugins);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSessionClose(SessionClose event)
|
||||
{
|
||||
executor.submit(this::refreshPlugins);
|
||||
}
|
||||
|
||||
private void refreshPlugins()
|
||||
{
|
||||
Multimap<ExternalPluginManifest, Plugin> loadedExternalPlugins = HashMultimap.create();
|
||||
for (Plugin p : pluginManager.getPlugins())
|
||||
{
|
||||
ExternalPluginManifest m = getExternalPluginManifest(p.getClass());
|
||||
if (m != null)
|
||||
{
|
||||
loadedExternalPlugins.put(m, p);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> installedIDs = getInstalledExternalPlugins();
|
||||
if (installedIDs.isEmpty() && loadedExternalPlugins.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
boolean startup = SplashScreen.isOpen();
|
||||
try
|
||||
{
|
||||
double splashStart = startup ? .60 : 0;
|
||||
double splashLength = startup ? .10 : 1;
|
||||
if (!startup)
|
||||
{
|
||||
SplashScreen.init();
|
||||
}
|
||||
|
||||
SplashScreen.stage(splashStart, null, "Downloading external plugins");
|
||||
Set<ExternalPluginManifest> externalPlugins = new HashSet<>();
|
||||
|
||||
RuneLite.PLUGINS_DIR.mkdirs();
|
||||
|
||||
List<ExternalPluginManifest> manifestList;
|
||||
try
|
||||
{
|
||||
manifestList = externalPluginClient.downloadManifest();
|
||||
Map<String, ExternalPluginManifest> manifests = manifestList
|
||||
.stream().collect(ImmutableMap.toImmutableMap(ExternalPluginManifest::getInternalName, Function.identity()));
|
||||
|
||||
Set<ExternalPluginManifest> needsDownload = new HashSet<>();
|
||||
Set<File> keep = new HashSet<>();
|
||||
|
||||
for (String name : installedIDs)
|
||||
{
|
||||
ExternalPluginManifest manifest = manifests.get(name);
|
||||
if (manifest != null)
|
||||
{
|
||||
externalPlugins.add(manifest);
|
||||
|
||||
if (!manifest.isValid())
|
||||
{
|
||||
needsDownload.add(manifest);
|
||||
}
|
||||
else
|
||||
{
|
||||
keep.add(manifest.getJarFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete old plugins
|
||||
File[] files = RuneLite.PLUGINS_DIR.listFiles();
|
||||
if (files != null)
|
||||
{
|
||||
for (File fi : files)
|
||||
{
|
||||
if (!keep.contains(fi))
|
||||
{
|
||||
fi.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int toDownload = needsDownload.stream().mapToInt(ExternalPluginManifest::getSize).sum();
|
||||
int downloaded = 0;
|
||||
|
||||
for (ExternalPluginManifest manifest : needsDownload)
|
||||
{
|
||||
HttpUrl url = RuneLiteProperties.getPluginHubBase().newBuilder()
|
||||
.addPathSegment(manifest.getInternalName())
|
||||
.addPathSegment(manifest.getCommit() + ".jar")
|
||||
.build();
|
||||
|
||||
try (Response res = RuneLiteAPI.CLIENT.newCall(new Request.Builder().url(url).build()).execute())
|
||||
{
|
||||
int fdownloaded = downloaded;
|
||||
downloaded += manifest.getSize();
|
||||
HashingInputStream his = new HashingInputStream(Hashing.sha256(),
|
||||
new CountingInputStream(res.body().byteStream(), i ->
|
||||
SplashScreen.stage(splashStart + (splashLength * .2), splashStart + (splashLength * .8),
|
||||
null, "Downloading " + manifest.getDisplayName(),
|
||||
i + fdownloaded, toDownload, true)));
|
||||
Files.asByteSink(manifest.getJarFile()).writeFrom(his);
|
||||
if (!his.hash().toString().equals(manifest.getHash()))
|
||||
{
|
||||
throw new VerificationException("Plugin " + manifest.getInternalName() + " didn't match its hash");
|
||||
}
|
||||
}
|
||||
catch (IOException | VerificationException e)
|
||||
{
|
||||
externalPlugins.remove(manifest);
|
||||
log.error("Unable to download external plugin \"{}\"", manifest.getInternalName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException | VerificationException e)
|
||||
{
|
||||
log.error("Unable to download external plugins", e);
|
||||
return;
|
||||
}
|
||||
|
||||
SplashScreen.stage(splashStart + (splashLength * .8), null, "Starting external plugins");
|
||||
|
||||
// TODO(abex): make sure the plugins get fully removed from the scheduler/eventbus/other managers (iterate and check classloader)
|
||||
Set<ExternalPluginManifest> add = new HashSet<>();
|
||||
for (ExternalPluginManifest ex : externalPlugins)
|
||||
{
|
||||
if (loadedExternalPlugins.removeAll(ex).size() <= 0)
|
||||
{
|
||||
add.add(ex);
|
||||
}
|
||||
}
|
||||
// list of loaded external plugins that aren't in the manifest
|
||||
Collection<Plugin> remove = loadedExternalPlugins.values();
|
||||
|
||||
for (Plugin p : remove)
|
||||
{
|
||||
log.info("Stopping external plugin \"{}\"", p.getClass());
|
||||
try
|
||||
{
|
||||
pluginManager.stopPlugin(p);
|
||||
}
|
||||
catch (PluginInstantiationException e)
|
||||
{
|
||||
log.warn("Unable to stop external plugin \"{}\"", p.getClass().getName(), e);
|
||||
}
|
||||
pluginManager.remove(p);
|
||||
}
|
||||
|
||||
for (ExternalPluginManifest manifest : add)
|
||||
{
|
||||
// I think this can't happen, but just in case
|
||||
if (!manifest.isValid())
|
||||
{
|
||||
log.warn("Invalid plugin for validated manifest: {}", manifest);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info("Loading external plugin \"{}\" version \"{}\" commit \"{}\"", manifest.getInternalName(), manifest.getVersion(), manifest.getCommit());
|
||||
|
||||
List<Plugin> newPlugins = null;
|
||||
try
|
||||
{
|
||||
ClassLoader cl = new ExternalPluginClassLoader(manifest, new URL[]{manifest.getJarFile().toURI().toURL()});
|
||||
List<Class<?>> clazzes = new ArrayList<>();
|
||||
for (String className : manifest.getPlugins())
|
||||
{
|
||||
clazzes.add(cl.loadClass(className));
|
||||
}
|
||||
|
||||
newPlugins = pluginManager.loadPlugins(clazzes, null);
|
||||
if (!startup)
|
||||
{
|
||||
pluginManager.loadDefaultPluginConfiguration(newPlugins);
|
||||
|
||||
for (Plugin p : newPlugins)
|
||||
{
|
||||
pluginManager.startPlugin(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("Unable to start or load external plugin \"{}\"", manifest.getInternalName(), e);
|
||||
if (newPlugins != null)
|
||||
{
|
||||
for (Plugin p : newPlugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
pluginManager.stopPlugin(p);
|
||||
}
|
||||
catch (Exception inner)
|
||||
{
|
||||
}
|
||||
pluginManager.remove(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!startup)
|
||||
{
|
||||
eventBus.post(new ExternalPluginsChanged(manifestList));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!startup)
|
||||
{
|
||||
SplashScreen.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getInstalledExternalPlugins()
|
||||
{
|
||||
String externalPluginsStr = configManager.getConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY);
|
||||
return Text.fromCSV(externalPluginsStr == null ? "" : externalPluginsStr);
|
||||
}
|
||||
|
||||
public void install(String key)
|
||||
{
|
||||
Set<String> plugins = new HashSet<>(getInstalledExternalPlugins());
|
||||
if (plugins.add(key))
|
||||
{
|
||||
configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY, Text.toCSV(plugins));
|
||||
executor.submit(this::refreshPlugins);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(String key)
|
||||
{
|
||||
Set<String> plugins = new HashSet<>(getInstalledExternalPlugins());
|
||||
if (plugins.remove(key))
|
||||
{
|
||||
configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, PLUGIN_LIST_KEY, Text.toCSV(plugins));
|
||||
executor.submit(this::refreshPlugins);
|
||||
}
|
||||
}
|
||||
|
||||
public void update()
|
||||
{
|
||||
executor.submit(this::refreshPlugins);
|
||||
}
|
||||
|
||||
public static ExternalPluginManifest getExternalPluginManifest(Class<? extends Plugin> plugin)
|
||||
{
|
||||
ClassLoader cl = plugin.getClassLoader();
|
||||
if (cl instanceof ExternalPluginClassLoader)
|
||||
{
|
||||
ExternalPluginClassLoader ecl = (ExternalPluginClassLoader) cl;
|
||||
return ecl.getManifest();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void loadBuiltin(Class<? extends Plugin>... plugins)
|
||||
{
|
||||
builtinExternals = plugins;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (c) 2019 Abex
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package net.runelite.client.externalplugins;
|
||||
|
||||
import com.google.common.hash.Hashing;
|
||||
import com.google.common.io.Files;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import net.runelite.client.RuneLite;
|
||||
|
||||
@Data
|
||||
public class ExternalPluginManifest
|
||||
{
|
||||
private String internalName;
|
||||
private String commit;
|
||||
private String hash;
|
||||
private int size;
|
||||
private String[] plugins;
|
||||
|
||||
private String displayName;
|
||||
private String version;
|
||||
private String author;
|
||||
private String description;
|
||||
private String[] tags;
|
||||
@EqualsAndHashCode.Exclude
|
||||
private URL support;
|
||||
private boolean hasIcon;
|
||||
|
||||
public boolean hasIcon()
|
||||
{
|
||||
return hasIcon;
|
||||
}
|
||||
|
||||
File getJarFile()
|
||||
{
|
||||
return new File(RuneLite.PLUGINS_DIR, internalName + commit + ".jar");
|
||||
}
|
||||
|
||||
boolean isValid()
|
||||
{
|
||||
File file = getJarFile();
|
||||
|
||||
try
|
||||
{
|
||||
if (file.exists())
|
||||
{
|
||||
String hash = Files.asByteSource(file).hash(Hashing.sha256()).toString();
|
||||
if (this.hash.equals(hash))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@
|
||||
package net.runelite.client.plugins;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.graph.Graph;
|
||||
import com.google.common.graph.GraphBuilder;
|
||||
@@ -49,6 +48,7 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
@@ -61,7 +61,6 @@ import net.runelite.client.events.SessionClose;
|
||||
import net.runelite.client.events.SessionOpen;
|
||||
import net.runelite.client.RuneLite;
|
||||
import net.runelite.client.config.Config;
|
||||
import net.runelite.client.config.ConfigGroup;
|
||||
import net.runelite.client.config.ConfigManager;
|
||||
import net.runelite.client.config.RuneLiteConfig;
|
||||
import net.runelite.client.eventbus.EventBus;
|
||||
@@ -90,8 +89,6 @@ public class PluginManager
|
||||
private final Provider<GameEventManager> sceneTileManager;
|
||||
private final List<Plugin> plugins = new CopyOnWriteArrayList<>();
|
||||
private final List<Plugin> activePlugins = new CopyOnWriteArrayList<>();
|
||||
private final String runeliteGroupName = RuneLiteConfig.class
|
||||
.getAnnotation(ConfigGroup.class).value();
|
||||
|
||||
@Setter
|
||||
boolean isOutdated;
|
||||
@@ -128,15 +125,22 @@ public class PluginManager
|
||||
|
||||
private void refreshPlugins()
|
||||
{
|
||||
loadDefaultPluginConfiguration();
|
||||
loadDefaultPluginConfiguration(null);
|
||||
getPlugins()
|
||||
.forEach(plugin -> executor.submit(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!startPlugin(plugin))
|
||||
if (isPluginEnabled(plugin) != activePlugins.contains(plugin))
|
||||
{
|
||||
stopPlugin(plugin);
|
||||
if (activePlugins.contains(plugin))
|
||||
{
|
||||
stopPlugin(plugin);
|
||||
}
|
||||
else
|
||||
{
|
||||
startPlugin(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (PluginInstantiationException e)
|
||||
@@ -162,11 +166,15 @@ public class PluginManager
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Config> getPluginConfigProxies()
|
||||
public List<Config> getPluginConfigProxies(Collection<Plugin> plugins)
|
||||
{
|
||||
List<Injector> injectors = new ArrayList<>();
|
||||
injectors.add(RuneLite.getInjector());
|
||||
getPlugins().forEach(pl -> injectors.add(pl.getInjector()));
|
||||
if (plugins == null)
|
||||
{
|
||||
injectors.add(RuneLite.getInjector());
|
||||
plugins = getPlugins();
|
||||
}
|
||||
plugins.forEach(pl -> injectors.add(pl.getInjector()));
|
||||
|
||||
List<Config> list = new ArrayList<>();
|
||||
for (Injector injector : injectors)
|
||||
@@ -185,20 +193,15 @@ public class PluginManager
|
||||
return list;
|
||||
}
|
||||
|
||||
public void loadDefaultPluginConfiguration()
|
||||
public void loadDefaultPluginConfiguration(Collection<Plugin> plugins)
|
||||
{
|
||||
for (Object config : getPluginConfigProxies())
|
||||
for (Object config : getPluginConfigProxies(plugins))
|
||||
{
|
||||
configManager.setDefaultConfiguration(config, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void loadCorePlugins() throws IOException
|
||||
{
|
||||
plugins.addAll(scanAndInstantiate(getClass().getClassLoader(), PLUGIN_PACKAGE));
|
||||
}
|
||||
|
||||
public void startCorePlugins()
|
||||
public void startPlugins()
|
||||
{
|
||||
List<Plugin> scannedPlugins = new ArrayList<>(plugins);
|
||||
int loaded = 0;
|
||||
@@ -219,37 +222,41 @@ public class PluginManager
|
||||
}
|
||||
}
|
||||
|
||||
List<Plugin> scanAndInstantiate(ClassLoader classLoader, String packageName) throws IOException
|
||||
public void loadCorePlugins() throws IOException, PluginInstantiationException
|
||||
{
|
||||
SplashScreen.stage(.59, null, "Loading Plugins");
|
||||
ClassPath classPath = ClassPath.from(getClass().getClassLoader());
|
||||
|
||||
List<Class<?>> plugins = classPath.getTopLevelClassesRecursive(PLUGIN_PACKAGE).stream()
|
||||
.map(ClassInfo::load)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
loadPlugins(plugins, (loaded, total) ->
|
||||
SplashScreen.stage(.60, .70, null, "Loading Plugins", loaded, total, false));
|
||||
}
|
||||
|
||||
public List<Plugin> loadPlugins(List<Class<?>> plugins, BiConsumer<Integer, Integer> onPluginLoaded) throws PluginInstantiationException
|
||||
{
|
||||
MutableGraph<Class<? extends Plugin>> graph = GraphBuilder
|
||||
.directed()
|
||||
.build();
|
||||
|
||||
List<Plugin> scannedPlugins = new ArrayList<>();
|
||||
ClassPath classPath = ClassPath.from(classLoader);
|
||||
|
||||
ImmutableSet<ClassInfo> classes = packageName == null ? classPath.getAllClasses()
|
||||
: classPath.getTopLevelClassesRecursive(packageName);
|
||||
for (ClassInfo classInfo : classes)
|
||||
for (Class<?> clazz : plugins)
|
||||
{
|
||||
Class<?> clazz = classInfo.load();
|
||||
PluginDescriptor pluginDescriptor = clazz.getAnnotation(PluginDescriptor.class);
|
||||
|
||||
if (pluginDescriptor == null)
|
||||
{
|
||||
if (clazz.getSuperclass() == Plugin.class)
|
||||
{
|
||||
log.warn("Class {} is a plugin, but has no plugin descriptor",
|
||||
clazz);
|
||||
log.warn("Class {} is a plugin, but has no plugin descriptor", clazz);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (clazz.getSuperclass() != Plugin.class)
|
||||
{
|
||||
log.warn("Class {} has plugin descriptor, but is not a plugin",
|
||||
clazz);
|
||||
log.warn("Class {} has plugin descriptor, but is not a plugin", clazz);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -280,20 +287,22 @@ public class PluginManager
|
||||
|
||||
if (Graphs.hasCycle(graph))
|
||||
{
|
||||
throw new RuntimeException("Plugin dependency graph contains a cycle!");
|
||||
throw new PluginInstantiationException("Plugin dependency graph contains a cycle!");
|
||||
}
|
||||
|
||||
List<Class<? extends Plugin>> sortedPlugins = topologicalSort(graph);
|
||||
sortedPlugins = Lists.reverse(sortedPlugins);
|
||||
|
||||
int loaded = 0;
|
||||
List<Plugin> newPlugins = new ArrayList<>();
|
||||
for (Class<? extends Plugin> pluginClazz : sortedPlugins)
|
||||
{
|
||||
Plugin plugin;
|
||||
try
|
||||
{
|
||||
plugin = instantiate(scannedPlugins, (Class<Plugin>) pluginClazz);
|
||||
scannedPlugins.add(plugin);
|
||||
plugin = instantiate(this.plugins, (Class<Plugin>) pluginClazz);
|
||||
newPlugins.add(plugin);
|
||||
this.plugins.add(plugin);
|
||||
}
|
||||
catch (PluginInstantiationException ex)
|
||||
{
|
||||
@@ -301,10 +310,13 @@ public class PluginManager
|
||||
}
|
||||
|
||||
loaded++;
|
||||
SplashScreen.stage(.60, .70, null, "Loading Plugins", loaded, sortedPlugins.size(), false);
|
||||
if (onPluginLoaded != null)
|
||||
{
|
||||
onPluginLoaded.accept(loaded, sortedPlugins.size());
|
||||
}
|
||||
}
|
||||
|
||||
return scannedPlugins;
|
||||
return newPlugins;
|
||||
}
|
||||
|
||||
public synchronized boolean startPlugin(Plugin plugin) throws PluginInstantiationException
|
||||
@@ -355,13 +367,11 @@ public class PluginManager
|
||||
|
||||
public synchronized boolean stopPlugin(Plugin plugin) throws PluginInstantiationException
|
||||
{
|
||||
if (!activePlugins.contains(plugin) || isPluginEnabled(plugin))
|
||||
if (!activePlugins.remove(plugin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
activePlugins.remove(plugin);
|
||||
|
||||
try
|
||||
{
|
||||
unschedule(plugin);
|
||||
@@ -395,13 +405,13 @@ public class PluginManager
|
||||
public void setPluginEnabled(Plugin plugin, boolean enabled)
|
||||
{
|
||||
final String keyName = plugin.getClass().getSimpleName().toLowerCase();
|
||||
configManager.setConfiguration(runeliteGroupName, keyName, String.valueOf(enabled));
|
||||
configManager.setConfiguration(RuneLiteConfig.GROUP_NAME, keyName, String.valueOf(enabled));
|
||||
}
|
||||
|
||||
public boolean isPluginEnabled(Plugin plugin)
|
||||
{
|
||||
final String keyName = plugin.getClass().getSimpleName().toLowerCase();
|
||||
final String value = configManager.getConfiguration(runeliteGroupName, keyName);
|
||||
final String value = configManager.getConfiguration(RuneLiteConfig.GROUP_NAME, keyName);
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
@@ -465,12 +475,12 @@ public class PluginManager
|
||||
return plugin;
|
||||
}
|
||||
|
||||
void add(Plugin plugin)
|
||||
public void add(Plugin plugin)
|
||||
{
|
||||
plugins.add(plugin);
|
||||
}
|
||||
|
||||
void remove(Plugin plugin)
|
||||
public void remove(Plugin plugin)
|
||||
{
|
||||
plugins.remove(plugin);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JFormattedTextField;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPasswordField;
|
||||
@@ -66,7 +67,10 @@ import net.runelite.client.config.Keybind;
|
||||
import net.runelite.client.config.ModifierlessKeybind;
|
||||
import net.runelite.client.config.Range;
|
||||
import net.runelite.client.eventbus.Subscribe;
|
||||
import net.runelite.client.events.ExternalPluginsChanged;
|
||||
import net.runelite.client.events.PluginChanged;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManager;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManifest;
|
||||
import net.runelite.client.plugins.PluginManager;
|
||||
import net.runelite.client.ui.ColorScheme;
|
||||
import net.runelite.client.ui.DynamicGridLayout;
|
||||
@@ -83,8 +87,8 @@ import net.runelite.client.util.Text;
|
||||
class ConfigPanel extends PluginPanel
|
||||
{
|
||||
private static final int SPINNER_FIELD_WIDTH = 6;
|
||||
private static final ImageIcon BACK_ICON;
|
||||
private static final ImageIcon BACK_ICON_HOVER;
|
||||
static final ImageIcon BACK_ICON;
|
||||
static final ImageIcon BACK_ICON_HOVER;
|
||||
|
||||
private final FixedWidthPanel mainPanel;
|
||||
private final JLabel title;
|
||||
@@ -99,6 +103,9 @@ class ConfigPanel extends PluginPanel
|
||||
@Inject
|
||||
private PluginManager pluginManager;
|
||||
|
||||
@Inject
|
||||
private ExternalPluginManager externalPluginManager;
|
||||
|
||||
@Inject
|
||||
private ColorPickerManager colorPickerManager;
|
||||
|
||||
@@ -162,7 +169,16 @@ class ConfigPanel extends PluginPanel
|
||||
title.setText(name);
|
||||
title.setForeground(Color.WHITE);
|
||||
title.setToolTipText("<html>" + name + ":<br>" + pluginConfig.getDescription() + "</html>");
|
||||
PluginListItem.addLabelPopupMenu(title, pluginConfig.createSupportMenuItem());
|
||||
|
||||
ExternalPluginManifest mf = pluginConfig.getExternalPluginManifest();
|
||||
JMenuItem uninstallItem = null;
|
||||
if (mf != null)
|
||||
{
|
||||
uninstallItem = new JMenuItem("Uninstall");
|
||||
uninstallItem.addActionListener(ev -> externalPluginManager.remove(mf.getInternalName()));
|
||||
}
|
||||
|
||||
PluginListItem.addLabelPopupMenu(title, pluginConfig.createSupportMenuItem(), uninstallItem);
|
||||
|
||||
if (pluginConfig.getPlugin() != null)
|
||||
{
|
||||
@@ -495,4 +511,15 @@ class ConfigPanel extends PluginPanel
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
private void onExternalPluginsChanged(ExternalPluginsChanged ev)
|
||||
{
|
||||
if (pluginManager.getPlugins().stream()
|
||||
.noneMatch(p -> p == this.pluginConfig.getPlugin()))
|
||||
{
|
||||
pluginList.getMuxer().popState();
|
||||
}
|
||||
SwingUtilities.invokeLater(this::rebuild);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import javax.swing.JMenuItem;
|
||||
import lombok.Value;
|
||||
import net.runelite.client.config.Config;
|
||||
import net.runelite.client.config.ConfigDescriptor;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManager;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManifest;
|
||||
import net.runelite.client.plugins.Plugin;
|
||||
import net.runelite.client.util.LinkBrowser;
|
||||
|
||||
@@ -60,10 +62,35 @@ class PluginConfigurationDescriptor
|
||||
*
|
||||
* @return A {@link JMenuItem} which opens the plugin's wiki page URL in the browser when clicked
|
||||
*/
|
||||
@Nullable
|
||||
JMenuItem createSupportMenuItem()
|
||||
{
|
||||
final JMenuItem menuItem = new JMenuItem("Wiki");
|
||||
ExternalPluginManifest mf = getExternalPluginManifest();
|
||||
if (mf != null)
|
||||
{
|
||||
if (mf.getSupport() == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
JMenuItem menuItem = new JMenuItem("Support");
|
||||
menuItem.addActionListener(e -> LinkBrowser.browse(mf.getSupport().toString()));
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
JMenuItem menuItem = new JMenuItem("Wiki");
|
||||
menuItem.addActionListener(e -> LinkBrowser.browse("https://github.com/runelite/runelite/wiki/" + name.replace(' ', '-')));
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
ExternalPluginManifest getExternalPluginManifest()
|
||||
{
|
||||
if (plugin == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ExternalPluginManager.getExternalPluginManifest(plugin.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
/*
|
||||
* Copyright (c) 2019 Abex
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package net.runelite.client.plugins.config;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToDoubleFunction;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.GroupLayout;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.LayoutStyle;
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.runelite.client.config.Config;
|
||||
import net.runelite.client.eventbus.Subscribe;
|
||||
import net.runelite.client.events.ExternalPluginsChanged;
|
||||
import net.runelite.client.externalplugins.ExternalPluginClient;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManager;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManifest;
|
||||
import net.runelite.client.plugins.Plugin;
|
||||
import net.runelite.client.plugins.PluginManager;
|
||||
import net.runelite.client.ui.ColorScheme;
|
||||
import net.runelite.client.ui.DynamicGridLayout;
|
||||
import net.runelite.client.ui.FontManager;
|
||||
import net.runelite.client.ui.PluginPanel;
|
||||
import net.runelite.client.ui.components.IconTextField;
|
||||
import net.runelite.client.util.ImageUtil;
|
||||
import net.runelite.client.util.LinkBrowser;
|
||||
import net.runelite.client.util.SwingUtil;
|
||||
import net.runelite.client.util.VerificationException;
|
||||
import org.apache.commons.text.similarity.JaroWinklerDistance;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
class PluginHubPanel extends PluginPanel
|
||||
{
|
||||
private static final ImageIcon MISSING_ICON;
|
||||
private static final ImageIcon HELP_ICON;
|
||||
private static final ImageIcon HELP_ICON_HOVER;
|
||||
private static final ImageIcon CONFIGURE_ICON;
|
||||
private static final ImageIcon CONFIGURE_ICON_HOVER;
|
||||
private static final Pattern SPACES = Pattern.compile(" +");
|
||||
private static final JaroWinklerDistance DISTANCE = new JaroWinklerDistance();
|
||||
|
||||
static
|
||||
{
|
||||
BufferedImage missingIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_missingicon.png");
|
||||
MISSING_ICON = new ImageIcon(missingIcon);
|
||||
|
||||
BufferedImage helpIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_help.png");
|
||||
HELP_ICON = new ImageIcon(helpIcon);
|
||||
HELP_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(helpIcon, -100));
|
||||
|
||||
BufferedImage configureIcon = ImageUtil.getResourceStreamFromClass(PluginHubPanel.class, "pluginhub_configure.png");
|
||||
CONFIGURE_ICON = new ImageIcon(configureIcon);
|
||||
CONFIGURE_ICON_HOVER = new ImageIcon(ImageUtil.alphaOffset(configureIcon, -100));
|
||||
}
|
||||
|
||||
private class PluginItem extends JPanel
|
||||
{
|
||||
private static final int HEIGHT = 70;
|
||||
private static final int ICON_WIDTH = 48;
|
||||
private static final int BOTTOM_LINE_HEIGHT = 16;
|
||||
static final float MIN_FILTER_SCORE = .8f;
|
||||
|
||||
private final ExternalPluginManifest manifest;
|
||||
|
||||
@Getter
|
||||
private final boolean installed;
|
||||
|
||||
@Getter
|
||||
private float filter;
|
||||
|
||||
PluginItem(ExternalPluginManifest newManifest, Collection<Plugin> loadedPlugins, boolean installed)
|
||||
{
|
||||
ExternalPluginManifest loaded = null;
|
||||
if (!loadedPlugins.isEmpty())
|
||||
{
|
||||
loaded = ExternalPluginManager.getExternalPluginManifest(loadedPlugins.iterator().next().getClass());
|
||||
}
|
||||
|
||||
manifest = newManifest == null ? loaded : newManifest;
|
||||
this.installed = installed;
|
||||
|
||||
setBackground(ColorScheme.DARKER_GRAY_COLOR);
|
||||
setOpaque(true);
|
||||
|
||||
GroupLayout layout = new GroupLayout(this);
|
||||
setLayout(layout);
|
||||
|
||||
JLabel pluginName = new JLabel(manifest.getDisplayName());
|
||||
pluginName.setFont(FontManager.getRunescapeBoldFont());
|
||||
pluginName.setToolTipText(manifest.getDisplayName());
|
||||
|
||||
JLabel author = new JLabel(manifest.getAuthor());
|
||||
author.setFont(FontManager.getRunescapeSmallFont());
|
||||
author.setToolTipText(manifest.getAuthor());
|
||||
|
||||
JLabel version = new JLabel(manifest.getVersion());
|
||||
version.setFont(FontManager.getRunescapeSmallFont());
|
||||
version.setToolTipText(manifest.getVersion());
|
||||
|
||||
JLabel description = new JLabel(manifest.getDescription());
|
||||
description.setToolTipText(manifest.getDescription());
|
||||
|
||||
JLabel icon = new JLabel();
|
||||
icon.setHorizontalAlignment(JLabel.CENTER);
|
||||
icon.setIcon(MISSING_ICON);
|
||||
if (manifest.hasIcon())
|
||||
{
|
||||
executor.submit(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
BufferedImage img = externalPluginClient.downloadIcon(manifest);
|
||||
|
||||
SwingUtilities.invokeLater(() ->
|
||||
{
|
||||
icon.setIcon(new ImageIcon(img));
|
||||
});
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
log.info("Cannot download icon for plugin \"{}\"", manifest.getInternalName(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
JButton help = new JButton(HELP_ICON);
|
||||
help.setRolloverIcon(HELP_ICON_HOVER);
|
||||
SwingUtil.removeButtonDecorations(help);
|
||||
help.setToolTipText("Help");
|
||||
help.setBorder(null);
|
||||
if (manifest.getSupport() == null)
|
||||
{
|
||||
help.setVisible(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
help.addActionListener(ev -> LinkBrowser.browse(manifest.getSupport().toString()));
|
||||
}
|
||||
|
||||
JButton configure = new JButton(CONFIGURE_ICON);
|
||||
configure.setRolloverIcon(CONFIGURE_ICON_HOVER);
|
||||
SwingUtil.removeButtonDecorations(configure);
|
||||
configure.setToolTipText("Configure");
|
||||
help.setBorder(null);
|
||||
if (loaded != null)
|
||||
{
|
||||
String search = null;
|
||||
if (loadedPlugins.size() > 1)
|
||||
{
|
||||
search = loaded.getInternalName();
|
||||
}
|
||||
else
|
||||
{
|
||||
Plugin plugin = loadedPlugins.iterator().next();
|
||||
Config cfg = pluginManager.getPluginConfigProxy(plugin);
|
||||
if (cfg == null)
|
||||
{
|
||||
search = loaded.getInternalName();
|
||||
}
|
||||
else
|
||||
{
|
||||
configure.addActionListener(l -> pluginListPanel.openConfigurationPanel(plugin));
|
||||
}
|
||||
}
|
||||
|
||||
if (search != null)
|
||||
{
|
||||
final String javaIsABadLanguage = search;
|
||||
configure.addActionListener(l -> pluginListPanel.openWithFilter(javaIsABadLanguage));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
configure.setVisible(false);
|
||||
}
|
||||
|
||||
boolean install = !installed;
|
||||
boolean update = loaded != null && newManifest != null && !newManifest.equals(loaded);
|
||||
boolean remove = !install && !update;
|
||||
JButton addrm = new JButton();
|
||||
if (install)
|
||||
{
|
||||
addrm.setText("Install");
|
||||
addrm.setBackground(new Color(0x28BE28));
|
||||
addrm.addActionListener(l -> externalPluginManager.install(manifest.getInternalName()));
|
||||
}
|
||||
else if (remove)
|
||||
{
|
||||
addrm.setText("Remove");
|
||||
addrm.setBackground(new Color(0xBE2828));
|
||||
addrm.addActionListener(l -> externalPluginManager.remove(manifest.getInternalName()));
|
||||
}
|
||||
else
|
||||
{
|
||||
assert update;
|
||||
addrm.setText("Update");
|
||||
addrm.setBackground(new Color(0x1F621F));
|
||||
addrm.addActionListener(l -> externalPluginManager.update());
|
||||
}
|
||||
addrm.setBorder(new LineBorder(addrm.getBackground().darker()));
|
||||
addrm.setFocusPainted(false);
|
||||
|
||||
layout.setHorizontalGroup(layout.createSequentialGroup()
|
||||
.addComponent(icon, ICON_WIDTH, ICON_WIDTH, ICON_WIDTH)
|
||||
.addGap(5)
|
||||
.addGroup(layout.createParallelGroup()
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(pluginName, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
|
||||
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(author, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE))
|
||||
.addComponent(description, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(version, 0, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
|
||||
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, 100)
|
||||
.addComponent(help, 0, 24, 24)
|
||||
.addComponent(configure, 0, 24, 24)
|
||||
.addComponent(addrm, 0, 50, GroupLayout.PREFERRED_SIZE)
|
||||
.addGap(5))));
|
||||
|
||||
layout.setVerticalGroup(layout.createParallelGroup()
|
||||
.addComponent(icon, HEIGHT, HEIGHT, HEIGHT)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addGap(5)
|
||||
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(pluginName)
|
||||
.addComponent(author))
|
||||
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
|
||||
.addComponent(description)
|
||||
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
|
||||
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
|
||||
.addComponent(version, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT)
|
||||
.addComponent(help, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT)
|
||||
.addComponent(configure, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT)
|
||||
.addComponent(addrm, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT, BOTTOM_LINE_HEIGHT))
|
||||
.addGap(5)));
|
||||
}
|
||||
|
||||
float setFilter(String[] filter)
|
||||
{
|
||||
ToDoubleFunction<String> match = r -> Stream.of(filter)
|
||||
.mapToDouble(l -> Math.pow(DISTANCE.apply(l, r), 2))
|
||||
.max()
|
||||
.orElse(0.D);
|
||||
|
||||
double sim = SPACES.splitAsStream(manifest.getDisplayName()).collect(Collectors.averagingDouble(match)) * 2;
|
||||
|
||||
if (manifest.getTags() != null)
|
||||
{
|
||||
sim += Stream.of(manifest.getTags()).mapToDouble(match).sum();
|
||||
}
|
||||
|
||||
return this.filter = (float) sim;
|
||||
}
|
||||
}
|
||||
|
||||
private final PluginListPanel pluginListPanel;
|
||||
private final ExternalPluginManager externalPluginManager;
|
||||
private final PluginManager pluginManager;
|
||||
private final ExternalPluginClient externalPluginClient;
|
||||
private final ScheduledExecutorService executor;
|
||||
|
||||
private final IconTextField searchBar;
|
||||
private final JLabel refreshing;
|
||||
private final JPanel mainPanel;
|
||||
private List<PluginItem> plugins = null;
|
||||
|
||||
@Inject
|
||||
PluginHubPanel(
|
||||
PluginListPanel pluginListPanel,
|
||||
ExternalPluginManager externalPluginManager,
|
||||
PluginManager pluginManager,
|
||||
ExternalPluginClient externalPluginClient,
|
||||
ScheduledExecutorService executor)
|
||||
{
|
||||
super(false);
|
||||
this.pluginListPanel = pluginListPanel;
|
||||
this.externalPluginManager = externalPluginManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.externalPluginClient = externalPluginClient;
|
||||
this.executor = executor;
|
||||
|
||||
{
|
||||
Object refresh = "this could just be a lambda, but no, it has to be abstracted";
|
||||
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), refresh);
|
||||
getActionMap().put(refresh, new AbstractAction()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
reloadPluginList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
GroupLayout layout = new GroupLayout(this);
|
||||
setLayout(layout);
|
||||
setBackground(ColorScheme.DARK_GRAY_COLOR);
|
||||
|
||||
searchBar = new IconTextField();
|
||||
searchBar.setIcon(IconTextField.Icon.SEARCH);
|
||||
searchBar.setBackground(ColorScheme.DARKER_GRAY_COLOR);
|
||||
searchBar.setHoverBackgroundColor(ColorScheme.DARK_GRAY_HOVER_COLOR);
|
||||
searchBar.getDocument().addDocumentListener(new DocumentListener()
|
||||
{
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e)
|
||||
{
|
||||
filter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e)
|
||||
{
|
||||
filter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e)
|
||||
{
|
||||
filter();
|
||||
}
|
||||
});
|
||||
|
||||
JLabel externalPluginWarning = new JLabel("<html>External plugins are not supported by the RuneLite Developers." +
|
||||
"They may cause bugs or instability.</html>");
|
||||
externalPluginWarning.setBackground(new Color(0xFFBB33));
|
||||
externalPluginWarning.setForeground(Color.BLACK);
|
||||
externalPluginWarning.setBorder(new EmptyBorder(5, 5, 5, 2));
|
||||
externalPluginWarning.setOpaque(true);
|
||||
|
||||
JLabel externalPluginWarning2 = new JLabel("Use at your own risk!");
|
||||
externalPluginWarning2.setHorizontalAlignment(JLabel.CENTER);
|
||||
externalPluginWarning2.setFont(FontManager.getRunescapeBoldFont());
|
||||
externalPluginWarning2.setBackground(externalPluginWarning.getBackground());
|
||||
externalPluginWarning2.setForeground(externalPluginWarning.getForeground());
|
||||
externalPluginWarning2.setBorder(new EmptyBorder(0, 5, 5, 5));
|
||||
externalPluginWarning2.setOpaque(true);
|
||||
|
||||
JButton backButton = new JButton(ConfigPanel.BACK_ICON);
|
||||
backButton.setRolloverIcon(ConfigPanel.BACK_ICON_HOVER);
|
||||
SwingUtil.removeButtonDecorations(backButton);
|
||||
backButton.setToolTipText("Back");
|
||||
backButton.addActionListener(l -> pluginListPanel.getMuxer().popState());
|
||||
|
||||
mainPanel = new JPanel();
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(0, 7, 7, 7));
|
||||
mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5));
|
||||
mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
refreshing = new JLabel("Loading...");
|
||||
refreshing.setHorizontalAlignment(JLabel.CENTER);
|
||||
|
||||
JPanel mainPanelWrapper = new JPanel();
|
||||
mainPanelWrapper.setLayout(new BorderLayout());
|
||||
mainPanelWrapper.add(mainPanel, BorderLayout.NORTH);
|
||||
mainPanelWrapper.add(refreshing, BorderLayout.CENTER);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane();
|
||||
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
scrollPane.setPreferredSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE));
|
||||
scrollPane.getViewport().setLayout(new BorderLayout());
|
||||
scrollPane.getViewport().add(mainPanelWrapper, BorderLayout.CENTER);
|
||||
|
||||
layout.setVerticalGroup(layout.createSequentialGroup()
|
||||
.addComponent(externalPluginWarning)
|
||||
.addComponent(externalPluginWarning2)
|
||||
.addGap(10)
|
||||
.addGroup(layout.createParallelGroup()
|
||||
.addComponent(backButton)
|
||||
.addComponent(searchBar))
|
||||
.addGap(10)
|
||||
.addComponent(scrollPane));
|
||||
|
||||
layout.setHorizontalGroup(layout.createParallelGroup()
|
||||
.addComponent(externalPluginWarning, 0, Short.MAX_VALUE, Short.MAX_VALUE)
|
||||
.addComponent(externalPluginWarning2, 0, Short.MAX_VALUE, Short.MAX_VALUE)
|
||||
.addGroup(layout.createSequentialGroup()
|
||||
.addComponent(backButton)
|
||||
.addComponent(searchBar)
|
||||
.addGap(10))
|
||||
.addComponent(scrollPane));
|
||||
|
||||
revalidate();
|
||||
|
||||
refreshing.setVisible(false);
|
||||
reloadPluginList();
|
||||
}
|
||||
|
||||
private void reloadPluginList()
|
||||
{
|
||||
if (refreshing.isVisible())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
refreshing.setVisible(true);
|
||||
mainPanel.removeAll();
|
||||
|
||||
executor.submit(() ->
|
||||
{
|
||||
List<ExternalPluginManifest> manifest;
|
||||
try
|
||||
{
|
||||
manifest = externalPluginClient.downloadManifest();
|
||||
}
|
||||
catch (IOException | VerificationException e)
|
||||
{
|
||||
log.error("", e);
|
||||
SwingUtilities.invokeLater(() ->
|
||||
{
|
||||
refreshing.setVisible(false);
|
||||
mainPanel.add(new JLabel("Downloading the plugin manifest failed"));
|
||||
|
||||
JButton retry = new JButton("Retry");
|
||||
retry.addActionListener(l -> reloadPluginList());
|
||||
mainPanel.add(retry);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reloadPluginList(manifest);
|
||||
});
|
||||
}
|
||||
|
||||
private void reloadPluginList(List<ExternalPluginManifest> manifest)
|
||||
{
|
||||
Map<String, ExternalPluginManifest> manifests = manifest.stream()
|
||||
.collect(ImmutableMap.toImmutableMap(ExternalPluginManifest::getInternalName, Function.identity()));
|
||||
|
||||
Multimap<String, Plugin> loadedPlugins = HashMultimap.create();
|
||||
for (Plugin p : pluginManager.getPlugins())
|
||||
{
|
||||
Class<? extends Plugin> clazz = p.getClass();
|
||||
ExternalPluginManifest mf = ExternalPluginManager.getExternalPluginManifest(clazz);
|
||||
if (mf != null)
|
||||
{
|
||||
loadedPlugins.put(mf.getInternalName(), p);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> installed = new HashSet<>(externalPluginManager.getInstalledExternalPlugins());
|
||||
|
||||
SwingUtilities.invokeLater(() ->
|
||||
{
|
||||
plugins = Sets.union(manifests.keySet(), loadedPlugins.keySet())
|
||||
.stream()
|
||||
.map(id -> new PluginItem(manifests.get(id), loadedPlugins.get(id), installed.contains(id)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
refreshing.setVisible(false);
|
||||
filter();
|
||||
});
|
||||
}
|
||||
|
||||
void filter()
|
||||
{
|
||||
if (refreshing.isVisible())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mainPanel.removeAll();
|
||||
|
||||
Stream<PluginItem> stream = plugins.stream();
|
||||
|
||||
String search = searchBar.getText();
|
||||
boolean isSearching = search != null && !search.trim().isEmpty();
|
||||
if (isSearching)
|
||||
{
|
||||
String[] searchArray = SPACES.split(search.toLowerCase());
|
||||
stream = stream
|
||||
.filter(p -> p.setFilter(searchArray) > PluginItem.MIN_FILTER_SCORE)
|
||||
.sorted(Comparator.comparing(PluginItem::getFilter));
|
||||
}
|
||||
else
|
||||
{
|
||||
stream = stream
|
||||
.sorted(Comparator.comparing(PluginItem::isInstalled));
|
||||
}
|
||||
|
||||
stream.forEach(mainPanel::add);
|
||||
mainPanel.revalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivate()
|
||||
{
|
||||
revalidate();
|
||||
searchBar.setText("");
|
||||
reloadPluginList();
|
||||
searchBar.requestFocusInWindow();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
private void onExternalPluginsChanged(ExternalPluginsChanged ev)
|
||||
{
|
||||
SwingUtilities.invokeLater(() -> reloadPluginList(ev.getLoadedManifest()));
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.swing.ImageIcon;
|
||||
@@ -48,6 +47,7 @@ import javax.swing.JToggleButton;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import lombok.Getter;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManifest;
|
||||
import net.runelite.client.ui.ColorScheme;
|
||||
import net.runelite.client.ui.PluginPanel;
|
||||
import net.runelite.client.util.ImageUtil;
|
||||
@@ -96,6 +96,11 @@ class PluginListItem extends JPanel
|
||||
Collections.addAll(keywords, pluginConfig.getName().toLowerCase().split(" "));
|
||||
Collections.addAll(keywords, pluginConfig.getDescription().toLowerCase().split(" "));
|
||||
Collections.addAll(keywords, pluginConfig.getTags());
|
||||
ExternalPluginManifest mf = pluginConfig.getExternalPluginManifest();
|
||||
if (mf != null)
|
||||
{
|
||||
keywords.add(mf.getInternalName());
|
||||
}
|
||||
|
||||
final List<JMenuItem> popupMenuItems = new ArrayList<>();
|
||||
|
||||
@@ -127,6 +132,7 @@ class PluginListItem extends JPanel
|
||||
buttonPanel.setLayout(new GridLayout(1, 2));
|
||||
add(buttonPanel, BorderLayout.LINE_END);
|
||||
|
||||
JMenuItem configMenuItem = null;
|
||||
if (pluginConfig.hasConfigurables())
|
||||
{
|
||||
JButton configButton = new JButton(CONFIG_ICON);
|
||||
@@ -145,13 +151,18 @@ class PluginListItem extends JPanel
|
||||
configButton.setVisible(true);
|
||||
configButton.setToolTipText("Edit plugin configuration");
|
||||
|
||||
final JMenuItem configMenuItem = new JMenuItem("Configure");
|
||||
configMenuItem = new JMenuItem("Configure");
|
||||
configMenuItem.addActionListener(e -> openGroupConfigPanel());
|
||||
popupMenuItems.add(configMenuItem);
|
||||
}
|
||||
|
||||
popupMenuItems.add(pluginConfig.createSupportMenuItem());
|
||||
addLabelPopupMenu(nameLabel, popupMenuItems);
|
||||
JMenuItem uninstallItem = null;
|
||||
if (mf != null)
|
||||
{
|
||||
uninstallItem = new JMenuItem("Uninstall");
|
||||
uninstallItem.addActionListener(ev -> pluginListPanel.getExternalPluginManager().remove(mf.getInternalName()));
|
||||
}
|
||||
|
||||
addLabelPopupMenu(nameLabel, configMenuItem, pluginConfig.createSupportMenuItem(), uninstallItem);
|
||||
add(nameLabel, BorderLayout.CENTER);
|
||||
|
||||
onOffToggle = new PluginToggleButton();
|
||||
@@ -214,18 +225,6 @@ class PluginListItem extends JPanel
|
||||
pluginListPanel.openConfigurationPanel(pluginConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mouseover effect to change the text of the passed label to {@link ColorScheme#BRAND_ORANGE} color, and
|
||||
* adds the passed menu item to a popup menu shown when the label is clicked.
|
||||
*
|
||||
* @param label The label to attach the mouseover and click effects to
|
||||
* @param menuItem The menu item to be shown when the label is clicked
|
||||
*/
|
||||
static void addLabelPopupMenu(final JLabel label, final JMenuItem menuItem)
|
||||
{
|
||||
addLabelPopupMenu(label, Collections.singletonList(menuItem));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mouseover effect to change the text of the passed label to {@link ColorScheme#BRAND_ORANGE} color, and
|
||||
* adds the passed menu items to a popup menu shown when the label is clicked.
|
||||
@@ -233,7 +232,7 @@ class PluginListItem extends JPanel
|
||||
* @param label The label to attach the mouseover and click effects to
|
||||
* @param menuItems The menu items to be shown when the label is clicked
|
||||
*/
|
||||
static void addLabelPopupMenu(final JLabel label, final Collection<JMenuItem> menuItems)
|
||||
static void addLabelPopupMenu(JLabel label, JMenuItem... menuItems)
|
||||
{
|
||||
final JPopupMenu menu = new JPopupMenu();
|
||||
final Color labelForeground = label.getForeground();
|
||||
@@ -241,6 +240,11 @@ class PluginListItem extends JPanel
|
||||
|
||||
for (final JMenuItem menuItem : menuItems)
|
||||
{
|
||||
if (menuItem == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some machines register mouseEntered through a popup menu, and do not register mouseExited when a popup
|
||||
// menu item is clicked, so reset the label's color when we click one of these options.
|
||||
menuItem.addActionListener(e -> label.setForeground(labelForeground));
|
||||
|
||||
@@ -37,6 +37,7 @@ import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
@@ -53,7 +54,9 @@ import net.runelite.client.config.ConfigManager;
|
||||
import net.runelite.client.config.RuneLiteConfig;
|
||||
import net.runelite.client.eventbus.EventBus;
|
||||
import net.runelite.client.eventbus.Subscribe;
|
||||
import net.runelite.client.events.ExternalPluginsChanged;
|
||||
import net.runelite.client.events.PluginChanged;
|
||||
import net.runelite.client.externalplugins.ExternalPluginManager;
|
||||
import net.runelite.client.plugins.Plugin;
|
||||
import net.runelite.client.plugins.PluginDescriptor;
|
||||
import net.runelite.client.plugins.PluginInstantiationException;
|
||||
@@ -78,8 +81,12 @@ class PluginListPanel extends PluginPanel
|
||||
private final Provider<ConfigPanel> configPanelProvider;
|
||||
private final List<PluginConfigurationDescriptor> fakePlugins = new ArrayList<>();
|
||||
|
||||
@Getter
|
||||
private final ExternalPluginManager externalPluginManager;
|
||||
|
||||
@Getter
|
||||
private final MultiplexingPluginPanel muxer;
|
||||
|
||||
private final IconTextField searchBar;
|
||||
private final JScrollPane scrollPane;
|
||||
private final FixedWidthPanel mainPanel;
|
||||
@@ -89,14 +96,17 @@ class PluginListPanel extends PluginPanel
|
||||
public PluginListPanel(
|
||||
ConfigManager configManager,
|
||||
PluginManager pluginManager,
|
||||
ExternalPluginManager externalPluginManager,
|
||||
ScheduledExecutorService executorService,
|
||||
EventBus eventBus,
|
||||
Provider<ConfigPanel> configPanelProvider)
|
||||
Provider<ConfigPanel> configPanelProvider,
|
||||
Provider<PluginHubPanel> pluginHubPanelProvider)
|
||||
{
|
||||
super(false);
|
||||
|
||||
this.configManager = configManager;
|
||||
this.pluginManager = pluginManager;
|
||||
this.externalPluginManager = externalPluginManager;
|
||||
this.executorService = executorService;
|
||||
this.configPanelProvider = configPanelProvider;
|
||||
|
||||
@@ -155,9 +165,15 @@ class PluginListPanel extends PluginPanel
|
||||
mainPanel.setLayout(new DynamicGridLayout(0, 1, 0, 5));
|
||||
mainPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
JButton externalPluginButton = new JButton("Plugin Hub");
|
||||
externalPluginButton.setBorder(new EmptyBorder(5, 5, 5, 5));
|
||||
externalPluginButton.setLayout(new BorderLayout(0, BORDER_OFFSET));
|
||||
externalPluginButton.addActionListener(l -> muxer.pushState(pluginHubPanelProvider.get()));
|
||||
|
||||
JPanel northPanel = new FixedWidthPanel();
|
||||
northPanel.setLayout(new BorderLayout());
|
||||
northPanel.add(mainPanel, BorderLayout.NORTH);
|
||||
northPanel.add(externalPluginButton, BorderLayout.SOUTH);
|
||||
|
||||
scrollPane = new JScrollPane(northPanel);
|
||||
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
@@ -225,6 +241,13 @@ class PluginListPanel extends PluginPanel
|
||||
scrollPane.getVerticalScrollBar().setValue(scrollBarPosition);
|
||||
}
|
||||
|
||||
void openWithFilter(String filter)
|
||||
{
|
||||
searchBar.setText(filter);
|
||||
onSearchBarChanged();
|
||||
muxer.pushState(this);
|
||||
}
|
||||
|
||||
private void onSearchBarChanged()
|
||||
{
|
||||
final String text = searchBar.getText();
|
||||
@@ -267,6 +290,18 @@ class PluginListPanel extends PluginPanel
|
||||
}
|
||||
}
|
||||
|
||||
void openConfigurationPanel(Plugin plugin)
|
||||
{
|
||||
for (PluginListItem pluginListItem : pluginList)
|
||||
{
|
||||
if (pluginListItem.getPluginConfig().getPlugin() == plugin)
|
||||
{
|
||||
openConfigurationPanel(pluginListItem.getPluginConfig());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void openConfigurationPanel(PluginConfigurationDescriptor plugin)
|
||||
{
|
||||
ConfigPanel panel = configPanelProvider.get();
|
||||
@@ -353,4 +388,9 @@ class PluginListPanel extends PluginPanel
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
private void onExternalPluginsChanged(ExternalPluginsChanged ev)
|
||||
{
|
||||
SwingUtilities.invokeLater(this::rebuildPluginList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,10 @@ import static net.runelite.client.rs.ClientUpdateCheckMode.NONE;
|
||||
import static net.runelite.client.rs.ClientUpdateCheckMode.VANILLA;
|
||||
import net.runelite.client.ui.FatalErrorDialog;
|
||||
import net.runelite.client.ui.SplashScreen;
|
||||
import net.runelite.client.util.CountingInputStream;
|
||||
import net.runelite.http.api.RuneLiteAPI;
|
||||
import net.runelite.http.api.worlds.World;
|
||||
import net.runelite.client.util.VerificationException;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
@@ -53,7 +53,7 @@ import javax.swing.border.EmptyBorder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.runelite.client.RuneLite;
|
||||
import net.runelite.client.RuneLiteProperties;
|
||||
import net.runelite.client.rs.VerificationException;
|
||||
import net.runelite.client.util.VerificationException;
|
||||
import net.runelite.client.util.LinkBrowser;
|
||||
|
||||
@Slf4j
|
||||
|
||||
@@ -22,18 +22,18 @@
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package net.runelite.client.rs;
|
||||
package net.runelite.client.util;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
class CountingInputStream extends FilterInputStream
|
||||
public class CountingInputStream extends FilterInputStream
|
||||
{
|
||||
private final IntConsumer changed;
|
||||
|
||||
CountingInputStream(InputStream in, IntConsumer changed)
|
||||
public CountingInputStream(InputStream in, IntConsumer changed)
|
||||
{
|
||||
super(in);
|
||||
this.changed = changed;
|
||||
@@ -22,7 +22,7 @@
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package net.runelite.client.rs;
|
||||
package net.runelite.client.util;
|
||||
|
||||
public class VerificationException extends Exception
|
||||
{
|
||||
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDDDCCAfSgAwIBAgIJAK8uBanmNQZaMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
|
||||
BAMMEHJ1bmVsaXRlLXBsdWdpbnMwHhcNMTkxMjEyMjEwNzUxWhcNMjUxMjEwMjEw
|
||||
NzUxWjAbMRkwFwYDVQQDDBBydW5lbGl0ZS1wbHVnaW5zMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEApu11OVANSU+pHaXRxB7fIZapucJ6BT46neicEixs
|
||||
NVPuK/QRVjO/G8F++MXFD/tlZUOEDllDN8uaHBIVwxilqEVYL7oX65Esl7qqC1TZ
|
||||
WGdjiMyYoK3CXWEWB4w+CdB31T7JG2HqH45ZsVs+U9OVWBkNkL5nNQNPOmZFd+3A
|
||||
yCb9nGlO7SxduiHpwh3CV19jY47y8tevyo5qpaBuQeWtu3vbpeer0kbDarwD3xoF
|
||||
yUMPRK518gxRUSmOpsSG5viQ731mKVCUUfIXz91d3s+kJYAjORHS4zJe9s+1dljp
|
||||
oLYNLkaP6m3CmNtC84OxkmognvZTNMbiQ3GQm/BK4sdjPQIDAQABo1MwUTAdBgNV
|
||||
HQ4EFgQUxrkiRXNd0OHPMkqgl9UgV1//OuQwHwYDVR0jBBgwFoAUxrkiRXNd0OHP
|
||||
Mkqgl9UgV1//OuQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA
|
||||
StPyblz3aqOM5z2KqHX1B7Z3Q8B58g55YSefpcfwWEc6LT4HCztszcZDteWpV3W2
|
||||
ERfemkGKgsDhQ0qkzIt7tS5eNN3PPj7RZZm7vl5HquQ1vC/33ri/Z3CEKzbW7knt
|
||||
i1iEpx8E9DKb9J9DjdKwNxSomOyCOFUt9YoQJs80xc1mwPDd6aWR3xwvnEUimkm+
|
||||
Dbj7HMOXLeyN810wkeWcT8nC5GhxH3ZAmVExBHsaIOB876RntzshBehjY8s8JQhw
|
||||
R+fT1e8EhYMM9ylYDk1KIWFWrAujjU04lS9tXZ5C2e7fr9R953XN6Y0PNM/taNTU
|
||||
GzwGroJZI02V+1ADO14rRA==
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 410 B |
Binary file not shown.
|
After Width: | Height: | Size: 477 B |
Binary file not shown.
|
After Width: | Height: | Size: 764 B |
@@ -11,3 +11,5 @@ runelite.wiki.building.link=https://github.com/runelite/runelite/wiki/Building-w
|
||||
runelite.dnschange.link=https://1.1.1.1/dns/
|
||||
runelite.jav_config=http://oldschool.runescape.com/jav_config.ws
|
||||
runelite.jav_config_backup=http://static.runelite.net/jav_config.ws
|
||||
runelite.pluginhub.url=https://repo.runelite.net/plugins
|
||||
runelite.pluginhub.version=${project.version}
|
||||
Reference in New Issue
Block a user