mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-04-29 22:24:34 +02:00
feat(Discord): Add Plugin loader
patch
This commit is contained in:
parent
5ffff1bd40
commit
9b083e6d84
@ -0,0 +1,207 @@
|
||||
|
||||
package app.revanced.integrations.discord.plugin;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import app.revanced.integrations.shared.react.BaseRemoteReactPreloadScriptBootstrapper;
|
||||
import com.facebook.react.bridge.CatalystInstanceImpl;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
public class BunnyBootstrapper extends BaseRemoteReactPreloadScriptBootstrapper {
|
||||
private WeakReference<Context> context;
|
||||
|
||||
private JSONObject theme;
|
||||
private final HashMap<String, Integer> RESOURCE_COLORS = new HashMap<>();
|
||||
private final HashMap<String, int[]> COMPONENT_COLORS = new HashMap<>();
|
||||
|
||||
|
||||
@Override
|
||||
protected void initialize(Context context) {
|
||||
this.context = new WeakReference<>(context);
|
||||
|
||||
download(
|
||||
"https://raw.githubusercontent.com/pyoncord/detta-builds/main/bunny.js",
|
||||
getWorkingDirectoryFile("bunny.bundle"),
|
||||
1024
|
||||
);
|
||||
|
||||
readThemeFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadPreloadScripts(CatalystInstanceImpl instance) {
|
||||
var config = new JSONObject();
|
||||
try {
|
||||
config.put("loaderName", "ReVanced");
|
||||
config.put("loaderVersion", "1.0.0");
|
||||
config.put("hasThemeSupport", true);
|
||||
buildThemeConfig(config);
|
||||
buildSysColorsConfig(config);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
instance.setGlobalVariable("__PYON_LOADER__", config.toString());
|
||||
super.loadPreloadScripts(instance);
|
||||
}
|
||||
|
||||
private void buildThemeConfig(JSONObject config) throws JSONException {
|
||||
config.put("storedTheme", theme);
|
||||
}
|
||||
|
||||
private void buildSysColorsConfig(JSONObject config) throws JSONException {
|
||||
boolean isSystemColorsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
|
||||
|
||||
config.put("isSysColorsSupported", isSystemColorsSupported);
|
||||
|
||||
if (isSystemColorsSupported) {
|
||||
var context = this.context.get();
|
||||
var resources = context.getResources();
|
||||
var packageName = context.getPackageName();
|
||||
|
||||
String[] accents = {"accent1", "accent2", "accent3", "neutral1", "neutral2"};
|
||||
int[] shades = {0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000};
|
||||
|
||||
var colors = new JSONObject() {{
|
||||
for (String accent : accents) {
|
||||
var accentColors = new JSONArray() {{
|
||||
for (int shade : shades) {
|
||||
@SuppressLint("DiscouragedApi")
|
||||
var colorResourceId = resources.getIdentifier(
|
||||
"system_" + accent + "_" + shade,
|
||||
"color",
|
||||
packageName
|
||||
);
|
||||
var color = colorResourceId == 0 ? 0 : context.getColor(colorResourceId);
|
||||
var hexColor = String.format("#%06X", (0xFFFFFF & color));
|
||||
|
||||
put(hexColor);
|
||||
}
|
||||
}};
|
||||
|
||||
put(accent, accentColors);
|
||||
}
|
||||
}};
|
||||
|
||||
config.put("sysColors", colors);
|
||||
}
|
||||
}
|
||||
|
||||
public int hookColorDark(String themeKey, int originalColor) {
|
||||
return getColor(themeKey, originalColor, true);
|
||||
}
|
||||
|
||||
public int hookColorLight(String themeKey, int originalColor) {
|
||||
return getColor(themeKey, originalColor, false);
|
||||
}
|
||||
|
||||
public int hookRawColor(Object contextOrResource, int id, int originalColor) {
|
||||
return readRawColor(contextOrResource, id, originalColor);
|
||||
}
|
||||
|
||||
private int getColor(String colorName, int originalColor, boolean isDark) {
|
||||
waitUntilInitialized();
|
||||
|
||||
var colors = COMPONENT_COLORS.get(colorName);
|
||||
if (colors == null) {
|
||||
return originalColor;
|
||||
}
|
||||
|
||||
if (isDark) {
|
||||
return colors[0];
|
||||
}
|
||||
|
||||
// Only if there are two colors in the array we return the light color
|
||||
if (colors.length == 2) {
|
||||
return colors[1];
|
||||
}
|
||||
|
||||
return originalColor;
|
||||
}
|
||||
|
||||
private void readThemeFile() {
|
||||
var themeFile = getWorkingDirectoryFile("pyoncord/theme.json");
|
||||
var legacyThemeFile = getWorkingDirectoryFile("vendetta_theme.json");
|
||||
|
||||
if (legacyThemeFile.exists() && !legacyThemeFile.renameTo(themeFile)) {
|
||||
throw new RuntimeException("Failed to rename theme file");
|
||||
}
|
||||
|
||||
if (!themeFile.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
theme = new JSONObject(read(themeFile, 256));
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
readThemeColors();
|
||||
}
|
||||
|
||||
private int hexStringToColorInt(String hexString) {
|
||||
var parsed = Color.parseColor(hexString);
|
||||
return (hexString.length() == 7) ? parsed : parsed & 0xFFFFFF | (parsed >>> 24);
|
||||
}
|
||||
|
||||
private void readThemeColors() {
|
||||
try {
|
||||
var data = theme.getJSONObject("data");
|
||||
|
||||
var jsonRawColors = data.getJSONObject("rawColors");
|
||||
var jsonSemanticColors = data.getJSONObject("semanticColors");
|
||||
|
||||
for (var colors = jsonRawColors.keys(); colors.hasNext(); ) {
|
||||
var colorKey = colors.next();
|
||||
int color = hexStringToColorInt(jsonRawColors.getString(colorKey));
|
||||
RESOURCE_COLORS.put(colorKey.toLowerCase(), color);
|
||||
}
|
||||
|
||||
for (var colors = jsonSemanticColors.keys(); colors.hasNext(); ) {
|
||||
var componentName = colors.next();
|
||||
var componentColors = jsonSemanticColors.getJSONArray(componentName);
|
||||
|
||||
int[] value;
|
||||
if (componentColors.length() == 1) {
|
||||
value = new int[]{hexStringToColorInt(componentColors.getString(0))};
|
||||
} else {
|
||||
value = new int[]{
|
||||
hexStringToColorInt(componentColors.getString(0)),
|
||||
hexStringToColorInt(componentColors.getString(1))
|
||||
};
|
||||
}
|
||||
|
||||
COMPONENT_COLORS.put(componentName, value);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int readRawColor(Object contextOrResource, int id, int originalColor) {
|
||||
waitUntilInitialized();
|
||||
|
||||
Resources resources;
|
||||
if (contextOrResource instanceof Context) {
|
||||
resources = ((Context) contextOrResource).getResources();
|
||||
} else {
|
||||
resources = (Resources) contextOrResource;
|
||||
}
|
||||
|
||||
var name = resources.getResourceEntryName(id);
|
||||
var color = RESOURCE_COLORS.get(name);
|
||||
|
||||
return Objects.requireNonNullElse(color, originalColor);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package app.revanced.integrations.discord.plugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import com.facebook.react.bridge.CatalystInstanceImpl;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class BunnyBootstrapperPatch {
|
||||
private final static BunnyBootstrapper INSTANCE = new BunnyBootstrapper();
|
||||
|
||||
public static void hookOnCreate(Activity mainActivity) {
|
||||
INSTANCE.hookOnCreate(mainActivity);
|
||||
}
|
||||
|
||||
public static void hookLoadScriptFromFile(CatalystInstanceImpl instance) {
|
||||
INSTANCE.hookLoadScriptFromFile(instance);
|
||||
}
|
||||
|
||||
public static int hookColorDark(String themeKey, int originalColor) {
|
||||
return INSTANCE.hookColorDark(themeKey, originalColor);
|
||||
}
|
||||
|
||||
public static int hookColorLight(String themeKey, int originalColor) {
|
||||
return INSTANCE.hookColorLight(themeKey, originalColor);
|
||||
}
|
||||
|
||||
public static int hookRawColor(Object contextOrResource, int id, int originalColor) {
|
||||
return INSTANCE.hookRawColor(contextOrResource, id, originalColor);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package app.revanced.integrations.shared.react;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import com.facebook.react.bridge.CatalystInstanceImpl;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public abstract class BaseReactPreloadScriptBootstrapper {
|
||||
private Thread initializeThread;
|
||||
private File workingDirectory;
|
||||
|
||||
protected abstract void initialize(Context context);
|
||||
|
||||
public final void hookOnCreate(Activity mainActivity) {
|
||||
workingDirectory = mainActivity.getFilesDir();
|
||||
if (!workingDirectory.exists() && !workingDirectory.mkdirs()) {
|
||||
throw new RuntimeException("Failed to create working directory");
|
||||
}
|
||||
|
||||
initializeThread = new Thread(() -> initialize(mainActivity));
|
||||
initializeThread.start();
|
||||
}
|
||||
|
||||
public final void hookLoadScriptFromFile(CatalystInstanceImpl instance) {
|
||||
waitUntilInitialized();
|
||||
loadPreloadScripts(instance);
|
||||
}
|
||||
|
||||
protected void loadPreloadScripts(CatalystInstanceImpl instance) {
|
||||
final var preloadScripts = workingDirectory.listFiles(pathname ->
|
||||
pathname.isFile() && pathname.getName().endsWith(".bundle"));
|
||||
assert preloadScripts != null;
|
||||
|
||||
for (final var preloadScript : preloadScripts) {
|
||||
final var path = preloadScript.getAbsolutePath();
|
||||
instance.loadPreloadScriptFromFile(path, path, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected void waitUntilInitialized() {
|
||||
try {
|
||||
initializeThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected final File getWorkingDirectoryFile(String name) {
|
||||
return new File(workingDirectory, name);
|
||||
}
|
||||
|
||||
protected final void write(InputStream inputStream, File file, int bufferSize) {
|
||||
try (final var fileOutputStream = new FileOutputStream(file)) {
|
||||
final var buffer = new byte[bufferSize];
|
||||
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
fileOutputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected final String read(File file, int bufferSize) {
|
||||
try (final var fileInputStream = new FileInputStream(file)) {
|
||||
final var buffer = new byte[bufferSize];
|
||||
final var stringBuilder = new StringBuilder();
|
||||
|
||||
int bytesRead;
|
||||
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
|
||||
stringBuilder.append(new String(buffer, 0, bytesRead));
|
||||
}
|
||||
|
||||
return stringBuilder.toString();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package app.revanced.integrations.shared.react;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BaseRemoteReactPreloadScriptBootstrapper extends BaseReactPreloadScriptBootstrapper {
|
||||
protected final void download(String url, File preloadScriptFile, int bufferSize) {
|
||||
final var eTagFile = getWorkingDirectoryFile(preloadScriptFile.getName() + ".etag");
|
||||
|
||||
try {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
if (eTagFile.exists() && preloadScriptFile.exists()) {
|
||||
connection.setRequestProperty("If-None-Match", read(eTagFile, 256));
|
||||
}
|
||||
connection.connect();
|
||||
|
||||
if (connection.getResponseCode() == 304) {
|
||||
connection.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (connection.getResponseCode() != 200) {
|
||||
throw new RuntimeException("Failed to download the preload script: " + connection.getResponseCode());
|
||||
}
|
||||
|
||||
final var eTagHeader = connection.getHeaderField("ETag");
|
||||
if (eTagHeader != null) {
|
||||
write(new ByteArrayInputStream(eTagHeader.getBytes()), eTagFile, 256);
|
||||
}
|
||||
|
||||
write(connection.getInputStream(), preloadScriptFile, bufferSize);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package com.facebook.react.bridge;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
public class CatalystInstanceImpl {
|
||||
public native void setGlobalVariable(String propName, String jsonValue);
|
||||
|
||||
public void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean loadSynchronously) {
|
||||
}
|
||||
|
||||
public void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) {
|
||||
}
|
||||
|
||||
public void loadPreloadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) {
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user