feat(scripting): integrated ui

This commit is contained in:
rhunk
2023-12-25 23:10:31 +01:00
parent 37becec350
commit 72c9b92a3e
16 changed files with 141 additions and 76 deletions

View File

@ -14,7 +14,6 @@ import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import me.rhunk.snapenhance.scripting.impl.IPCListeners import me.rhunk.snapenhance.scripting.impl.IPCListeners
import me.rhunk.snapenhance.scripting.impl.ManagerIPC import me.rhunk.snapenhance.scripting.impl.ManagerIPC
import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig
import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -67,7 +66,6 @@ class RemoteScriptManager(
putConst("currentSide", this, BindingSide.MANAGER.key) putConst("currentSide", this, BindingSide.MANAGER.key)
module.registerBindings( module.registerBindings(
ManagerIPC(ipcListeners), ManagerIPC(ipcListeners),
InterfaceManager(),
ManagerScriptConfig(this@RemoteScriptManager) ManagerScriptConfig(this@RemoteScriptManager)
) )
} }

View File

@ -1,15 +0,0 @@
package me.rhunk.snapenhance.scripting.impl.ui.components.impl
import me.rhunk.snapenhance.scripting.impl.ui.components.Node
import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType
enum class ActionType {
LAUNCHED,
DISPOSE
}
class ActionNode(
val actionType: ActionType,
val key: Any = Unit,
val callback: () -> Unit
): Node(NodeType.ACTION)

View File

@ -20,7 +20,9 @@ import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface
import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface
import me.rhunk.snapenhance.ui.manager.Section import me.rhunk.snapenhance.ui.manager.Section
import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper import me.rhunk.snapenhance.ui.util.ActivityLauncherHelper
import me.rhunk.snapenhance.ui.util.chooseFolder import me.rhunk.snapenhance.ui.util.chooseFolder
@ -140,22 +142,14 @@ class ScriptsSection : Section() {
@Composable @Composable
fun ScriptSettings(script: ModuleInfo) { fun ScriptSettings(script: ModuleInfo) {
var settingsError by remember {
mutableStateOf(null as Throwable?)
}
val settingsInterface = remember { val settingsInterface = remember {
val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null
runCatching { (module.getBinding(InterfaceManager::class))?.buildInterface(EnumScriptInterface.SETTINGS)
(module.getBinding(InterfaceManager::class))?.buildInterface("settings")
}.onFailure {
settingsError = it
}.getOrNull()
} }
if (settingsInterface == null) { if (settingsInterface == null) {
Text( Text(
text = settingsError?.message ?: "This module does not have any settings", text = "This module does not have any settings",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(8.dp) modifier = Modifier.padding(8.dp)
) )

View File

@ -703,6 +703,10 @@
"name": "Auto Reload", "name": "Auto Reload",
"description": "Automatically reloads scripts when they change" "description": "Automatically reloads scripts when they change"
}, },
"integrated_ui": {
"name": "Integrated UI",
"description": "Allows scripts to add custom UI components to Snapchat"
},
"disable_log_anonymization": { "disable_log_anonymization": {
"name": "Disable Log Anonymization", "name": "Disable Log Anonymization",
"description": "Disables the anonymization of logs" "description": "Disables the anonymization of logs"

View File

@ -7,5 +7,6 @@ class Scripting : ConfigContainer() {
val developerMode = boolean("developer_mode", false) { requireRestart() } val developerMode = boolean("developer_mode", false) { requireRestart() }
val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER); requireRestart() } val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER); requireRestart() }
val autoReload = unique("auto_reload", "snapchat_only", "all") val autoReload = unique("auto_reload", "snapchat_only", "all")
val integratedUI = boolean("integrated_ui", false) { requireRestart() }
val disableLogAnonymization = boolean("disable_log_anonymization", false) { requireRestart() } val disableLogAnonymization = boolean("disable_log_anonymization", false) { requireRestart() }
} }

View File

@ -11,6 +11,7 @@ import me.rhunk.snapenhance.common.scripting.ktx.scriptable
import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import me.rhunk.snapenhance.common.scripting.type.Permissions import me.rhunk.snapenhance.common.scripting.type.Permissions
import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
import org.mozilla.javascript.Function import org.mozilla.javascript.Function
import org.mozilla.javascript.NativeJavaObject import org.mozilla.javascript.NativeJavaObject
import org.mozilla.javascript.ScriptableObject import org.mozilla.javascript.ScriptableObject
@ -53,6 +54,7 @@ class JSModule(
registerBindings( registerBindings(
JavaInterfaces(), JavaInterfaces(),
InterfaceManager(),
) )
moduleObject.putFunction("setField") { args -> moduleObject.putFunction("setField") { args ->

View File

@ -0,0 +1,11 @@
package me.rhunk.snapenhance.common.scripting.ui
import me.rhunk.snapenhance.common.scripting.bindings.BindingSide
enum class EnumScriptInterface(
val key: String,
val side: BindingSide
) {
SETTINGS("settings", BindingSide.MANAGER),
FRIEND_FEED_CONTEXT_MENU("friendFeedContextMenu", BindingSide.CORE),
}

View File

@ -1,13 +1,14 @@
package me.rhunk.snapenhance.scripting.impl.ui package me.rhunk.snapenhance.common.scripting.ui
import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding
import me.rhunk.snapenhance.common.scripting.bindings.BindingSide import me.rhunk.snapenhance.common.scripting.bindings.BindingSide
import me.rhunk.snapenhance.common.scripting.ktx.contextScope import me.rhunk.snapenhance.common.scripting.ktx.contextScope
import me.rhunk.snapenhance.scripting.impl.ui.components.Node import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject
import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType import me.rhunk.snapenhance.common.scripting.ui.components.Node
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode import me.rhunk.snapenhance.common.scripting.ui.components.NodeType
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.RowColumnNode import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionType
import me.rhunk.snapenhance.common.scripting.ui.components.impl.RowColumnNode
import org.mozilla.javascript.Function import org.mozilla.javascript.Function
import org.mozilla.javascript.annotations.JSFunction import org.mozilla.javascript.annotations.JSFunction
@ -73,24 +74,36 @@ class InterfaceBuilder {
class InterfaceManager : AbstractBinding("interface-manager", BindingSide.MANAGER) { class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON) {
private val interfaces = mutableMapOf<String, () -> InterfaceBuilder?>() private val interfaces = mutableMapOf<String, (args: Map<String, Any?>) -> InterfaceBuilder?>()
fun buildInterface(name: String): InterfaceBuilder? { fun buildInterface(scriptInterface: EnumScriptInterface, args: Map<String, Any?> = emptyMap()): InterfaceBuilder? {
return interfaces[name]?.invoke() return runCatching {
interfaces[scriptInterface.key]?.invoke(args)
}.onFailure {
context.runtime.logger.error("Failed to build interface ${scriptInterface.key} for ${context.moduleInfo.name}", it)
}.getOrNull()
} }
override fun onDispose() { override fun onDispose() {
interfaces.clear() interfaces.clear()
} }
fun hasInterface(scriptInterfaces: EnumScriptInterface): Boolean {
return interfaces.containsKey(scriptInterfaces.key)
}
@Suppress("unused") @Suppress("unused")
@JSFunction fun create(name: String, callback: Function) { @JSFunction fun create(name: String, callback: Function) {
interfaces[name] = { interfaces[name] = { args ->
val interfaceBuilder = InterfaceBuilder() val interfaceBuilder = InterfaceBuilder()
runCatching { runCatching {
contextScope { contextScope {
callback.call(this, callback, callback, arrayOf(interfaceBuilder)) callback.call(this, callback, callback, arrayOf(interfaceBuilder, scriptableObject {
args.forEach { (key, value) ->
putConst(key,this, value)
}
}))
} }
interfaceBuilder interfaceBuilder
}.onFailure { }.onFailure {

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.ui.manager.sections.scripting package me.rhunk.snapenhance.common.scripting.ui
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
@ -13,11 +13,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.scripting.impl.ui.InterfaceBuilder import me.rhunk.snapenhance.common.scripting.ui.components.Node
import me.rhunk.snapenhance.scripting.impl.ui.components.Node import me.rhunk.snapenhance.common.scripting.ui.components.NodeType
import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionType
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType
import kotlin.math.abs import kotlin.math.abs

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.scripting.impl.ui.components package me.rhunk.snapenhance.common.scripting.ui.components
open class Node( open class Node(
val type: NodeType, val type: NodeType,

View File

@ -1,4 +1,5 @@
package me.rhunk.snapenhance.scripting.impl.ui.components package me.rhunk.snapenhance.common.scripting.ui.components
enum class NodeType { enum class NodeType {
ROW, ROW,
COLUMN, COLUMN,

View File

@ -0,0 +1,15 @@
package me.rhunk.snapenhance.common.scripting.ui.components.impl
import me.rhunk.snapenhance.common.scripting.ui.components.Node
import me.rhunk.snapenhance.common.scripting.ui.components.NodeType
enum class ActionType {
LAUNCHED,
DISPOSE
}
class ActionNode(
val actionType: ActionType,
val key: Any = Unit,
val callback: () -> Unit
): Node(NodeType.ACTION)

View File

@ -1,9 +1,9 @@
package me.rhunk.snapenhance.scripting.impl.ui.components.impl package me.rhunk.snapenhance.common.scripting.ui.components.impl
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import me.rhunk.snapenhance.scripting.impl.ui.components.Node import me.rhunk.snapenhance.common.scripting.ui.components.Node
import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType import me.rhunk.snapenhance.common.scripting.ui.components.NodeType
class RowColumnNode( class RowColumnNode(

View File

@ -25,7 +25,7 @@ import me.rhunk.snapenhance.core.features.impl.tweaks.UnsaveableMessages
import me.rhunk.snapenhance.core.features.impl.ui.* import me.rhunk.snapenhance.core.features.impl.ui.*
import me.rhunk.snapenhance.core.logger.CoreLogger import me.rhunk.snapenhance.core.logger.CoreLogger
import me.rhunk.snapenhance.core.manager.Manager import me.rhunk.snapenhance.core.manager.Manager
import me.rhunk.snapenhance.core.ui.menu.impl.MenuViewInjector import me.rhunk.snapenhance.core.ui.menu.MenuViewInjector
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis

View File

@ -1,4 +1,4 @@
package me.rhunk.snapenhance.core.ui.menu.impl package me.rhunk.snapenhance.core.ui.menu
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.Gravity import android.view.Gravity
@ -6,12 +6,13 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ScrollView
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
import me.rhunk.snapenhance.core.ui.ViewTagState import me.rhunk.snapenhance.core.ui.ViewTagState
import me.rhunk.snapenhance.core.ui.menu.AbstractMenu import me.rhunk.snapenhance.core.ui.menu.impl.*
import me.rhunk.snapenhance.core.util.ktx.getIdentifier import me.rhunk.snapenhance.core.util.ktx.getIdentifier
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -27,14 +28,18 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
@SuppressLint("ResourceType") @SuppressLint("ResourceType")
override fun asyncOnActivityCreate() { override fun asyncOnActivityCreate() {
menuMap[OperaContextActionMenu::class] = OperaContextActionMenu() arrayOf(
menuMap[OperaDownloadIconMenu::class] = OperaDownloadIconMenu() OperaContextActionMenu(),
menuMap[SettingsGearInjector::class] = SettingsGearInjector() OperaDownloadIconMenu(),
menuMap[FriendFeedInfoMenu::class] = FriendFeedInfoMenu() SettingsGearInjector(),
menuMap[ChatActionMenu::class] = ChatActionMenu() FriendFeedInfoMenu(),
menuMap[SettingsMenu::class] = SettingsMenu() ChatActionMenu(),
SettingsMenu()
menuMap.values.forEach { it.context = context; it.init() } ).forEach {
menuMap[it::class] = it.also {
it.context = context; it.init()
}
}
val messaging = context.feature(Messaging::class) val messaging = context.feature(Messaging::class)
@ -90,26 +95,29 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
}) })
} }
val viewList = mutableListOf<View>()
context.runOnUiThread { context.runOnUiThread {
menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view -> injectedLayout.addView(ScrollView(injectedLayout.context).apply {
view.layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT
).apply { ).apply {
setMargins(0, 5, 0, 5) weight = 1f;
setMargins(0, 100, 0, 0)
} }
viewList.add(view)
}
viewList.add(View(injectedLayout.context).apply { addView(LinearLayout(context).apply {
layoutParams = LinearLayout.LayoutParams( orientation = LinearLayout.VERTICAL
ViewGroup.LayoutParams.MATCH_PARENT, menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view ->
30 view.layoutParams = LinearLayout.LayoutParams(
) ViewGroup.LayoutParams.MATCH_PARENT,
}) ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
viewList.reversed().forEach { injectedLayout.addView(it, 0) } setMargins(0, 5, 0, 5)
}
addView(view)
}
})
}, 0)
} }
event.view = injectedLayout event.view = injectedLayout

View File

@ -9,8 +9,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.LinearLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Switch import android.widget.Switch
import androidx.compose.runtime.remember
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -21,6 +23,10 @@ import me.rhunk.snapenhance.common.data.MessageUpdate
import me.rhunk.snapenhance.common.database.impl.ConversationMessage import me.rhunk.snapenhance.common.database.impl.ConversationMessage
import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.common.database.impl.FriendInfo
import me.rhunk.snapenhance.common.database.impl.UserConversationLink import me.rhunk.snapenhance.common.database.impl.UserConversationLink
import me.rhunk.snapenhance.common.scripting.ui.EnumScriptInterface
import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
import me.rhunk.snapenhance.common.scripting.ui.ScriptInterface
import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
@ -326,5 +332,33 @@ class FriendFeedInfoMenu : AbstractMenu() {
} }
}) })
} }
if (context.config.scripting.integratedUI.get()) {
context.scriptRuntime.eachModule {
val interfaceManager = getBinding(InterfaceManager::class)
?.takeIf {
it.hasInterface(EnumScriptInterface.FRIEND_FEED_CONTEXT_MENU)
} ?: return@eachModule
viewConsumer(LinearLayout(view.context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
applyTheme(view.width, hasRadius = true)
orientation = LinearLayout.VERTICAL
addView(createComposeView(view.context) {
ScriptInterface(interfaceBuilder = remember {
interfaceManager.buildInterface(EnumScriptInterface.FRIEND_FEED_CONTEXT_MENU, mapOf(
"conversationId" to conversationId,
"userId" to targetUser
))
} ?: return@createComposeView)
})
})
}
}
} }
} }