commit f830ce13c697b3f377afb64ba81436fbd454e0d6
parent 69e5ca74eead5e531cccbd6917f0387358fd6d30
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date: Thu, 25 Apr 2024 15:21:42 +0200
feat(composer_hooks): opera download button
Diffstat:
2 files changed, 125 insertions(+), 53 deletions(-)
diff --git a/core/src/main/assets/composer/loader.js b/core/src/main/assets/composer/loader.js
@@ -9,51 +9,130 @@ if (config.composerLogs) {
console.info("loader.js loaded");
}
-if (config.bypassCameraRollLimit) {
- (module => {
- module.MultiSelectClickHandler = new Proxy(module.MultiSelectClickHandler, {
- construct: function(target, args, newTarget) {
- args[1].selectionLimit = 9999999;
- return Reflect.construct(target, args, newTarget);
- },
- });
- })(require('memories_ui/src/clickhandlers/MultiSelectClickHandler'))
+// Composer imports
+
+const jsx = require('composer_core/src/JSX').jsx;
+const assetCatalog = require("composer_core/src/AssetCatalog")
+const style = require("composer_core/src/Style");
+const colors = require("coreui/src/styles/semanticColors");
+
+function dumpObject(obj, indent = 0) {
+ if (typeof obj !== "object") return console.log(obj);
+ let prefix = ""
+ for (let i = 0; i < indent; i++) {
+ prefix += " ";
+ }
+ for (let key of Object.keys(obj)) {
+ try {
+ console.log(prefix, key, typeof obj[key], obj[key]);
+ if (key == "renderer") continue
+ if (typeof obj[key] === "object" && indent < 10) dumpObject(obj[key], indent + 1);
+ } catch (e) {
+ }
+ }
+}
+
+function proxyProperty(module, functionName, handler) {
+ if (!module || !module[functionName]) {
+ console.warn("Function not found", functionName);
+ return;
+ }
+ module[functionName] = new Proxy(module[functionName], {
+ apply: (a, b, c) => handler(a, b, c),
+ construct: (a, b, c) => handler(a, b, c)
+ });
}
-(module => {
- function onComponentPreRender(component, viewModel) {
- const componentName = component.constructor.name;
+function interceptComponent(moduleName, className, functions) {
+ proxyProperty(require(moduleName), className, (target, args, newTarget) => {
+ let initProxy = functions["<init>"]
+ let component;
- if (componentName == "ProfileIdentityView" && config.showFirstCreatedUsername) {
- let userInfo = callExport("getFriendInfoByUsername", viewModel.username);
+ if (initProxy) {
+ initProxy(args, (newArgs) => {
+ component = Reflect.construct(target, newArgs || args, newTarget);
+ });
+ } else {
+ component = Reflect.construct(target, args, newTarget);
+ }
- if (userInfo) {
- let userInfoJson = JSON.parse(userInfo);
- let firstCreatedUsername = userInfoJson.username.split("|")[0];
- if (firstCreatedUsername != viewModel.username) {
- viewModel.username += " (" + firstCreatedUsername + ")";
+ for (let funcName of Object.keys(functions)) {
+ if (funcName == "<init>" || !component[funcName]) continue
+ proxyProperty(component, funcName, (target, thisArg, argumentsList) => {
+ let result;
+ try {
+ functions[funcName](component, argumentsList, (newArgs) => {
+ result = Reflect.apply(target, thisArg, newArgs || argumentsList);
+ });
+ } catch (e) {
+ console.error("Error in", funcName, e);
}
+ return result;
+ });
+ }
+
+ return component;
+ })
+}
+
+if (config.bypassCameraRollLimit) {
+ interceptComponent(
+ 'memories_ui/src/clickhandlers/MultiSelectClickHandler',
+ 'MultiSelectClickHandler',
+ {
+ "<init>": (args, superCall) => {
+ args[1].selectionLimit = 9999999;
+ superCall();
}
}
+ )
+}
- return false
- }
-
- function onComponentPostRender(component, viewModel) {
- }
+if (config.operaDownloadButton) {
+ const downloadIcon = assetCatalog.loadCatalog("share_sheet/res").downloadIcon
- module.Component = new Proxy(module.Component, {
- construct: function(target, args, newTarget) {
- let component = Reflect.construct(target, args, newTarget);
- component.onRender = new Proxy(component.onRender, {
- apply: function(target, thisArg, argumentsList) {
- if (onComponentPreRender(component, thisArg.viewModel || {})) return;
- let result = Reflect.apply(target, thisArg, argumentsList);
- onComponentPostRender(component, thisArg.viewModel || {});
- return result;
+ interceptComponent(
+ 'context_chrome_header/src/ChromeHeaderRenderer',
+ 'ChromeHeaderRenderer',
+ {
+ onRenderBaseHeader: (component, args, render) => {
+ render()
+ jsx.beginRender(jsx.makeNodePrototype("image"))
+ jsx.setAttributeStyle("style", new style.Style({
+ height: 32,
+ marginTop: 4,
+ marginLeft: 8,
+ marginRight: 12,
+ objectFit: "contain",
+ tint: colors.SemanticColor.Icon.PRIMARY
+ }))
+ jsx.setAttribute("src", downloadIcon)
+ jsx.setAttributeFunction("onTap", () => callExport("downloadLastOperaMedia", false))
+ jsx.setAttributeFunction("onLongPress", () => callExport("downloadLastOperaMedia", true))
+ jsx.endRender()
+ }
+ }
+ )
+}
+
+if (config.showFirstCreatedUsername) {
+ interceptComponent(
+ 'common_profile/src/identity/ProfileIdentityView',
+ 'ProfileIdentityView',
+ {
+ onRender: (component, _, render) => {
+ if (component.viewModel) {
+ let userInfo = callExport("getFriendInfoByUsername", component.viewModel.username);
+ if (userInfo) {
+ let userInfoJson = JSON.parse(userInfo);
+ let firstCreatedUsername = userInfoJson.username.split("|")[0];
+ if (firstCreatedUsername != component.viewModel.username) {
+ component.viewModel.username += " (" + firstCreatedUsername + ")";
+ }
+ }
}
- });
- return component;
+ render();
+ }
}
- })
-})(require('composer_core/src/Component'))
+ )
+}
diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt
@@ -30,6 +30,7 @@ import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
+import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.Hooker
import me.rhunk.snapenhance.core.util.hook.hook
@@ -131,12 +132,13 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACT
}
private fun getConfig(): Map<String, Any> {
- return HashMap<String, Any>().apply {
- put("bypassCameraRollLimit", config.bypassCameraRollLimit.get())
- put("showFirstCreatedUsername", config.showFirstCreatedUsername.get())
- put("composerConsole", config.composerConsole.get())
- put("composerLogs", config.composerLogs.get())
- }
+ return mapOf<String, Any>(
+ "operaDownloadButton" to context.config.downloader.operaDownloadButton.get(),
+ "bypassCameraRollLimit" to config.bypassCameraRollLimit.get(),
+ "showFirstCreatedUsername" to config.showFirstCreatedUsername.get(),
+ "composerConsole" to config.composerConsole.get(),
+ "composerLogs" to config.composerLogs.get()
+ )
}
private fun handleExportCall(composerMarshaller: ComposerMarshaller): Boolean {
@@ -150,6 +152,7 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACT
if (argc < 2) return false
context.shortToast(composerMarshaller.getUntyped(1) as? String ?: return false)
}
+ "downloadLastOperaMedia" -> context.feature(MediaDownloader::class).downloadLastOperaMediaAsync(composerMarshaller.getUntyped(1) == true)
"getFriendInfoByUsername" -> {
if (argc < 2) return false
val username = composerMarshaller.getUntyped(1) as? String ?: return false
@@ -176,16 +179,6 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACT
"error" -> context.log.error(message, tag)
}
}
- "eval" -> {
- if (argc < 2) return false
- runCatching {
- composerMarshaller.pushUntyped(context.native.composerEval(
- composerMarshaller.getUntyped(1) as? String ?: return false
- ))
- }.onFailure {
- composerMarshaller.pushUntyped(it.toString())
- }
- }
else -> context.log.warn("Unknown action: $action", "Composer")
}