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.ManagerIPC
import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig
import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager
import java.io.File
import java.io.InputStream
import kotlin.system.exitProcess
@ -67,7 +66,6 @@ class RemoteScriptManager(
putConst("currentSide", this, BindingSide.MANAGER.key)
module.registerBindings(
ManagerIPC(ipcListeners),
InterfaceManager(),
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.launch
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.util.ActivityLauncherHelper
import me.rhunk.snapenhance.ui.util.chooseFolder
@ -140,22 +142,14 @@ class ScriptsSection : Section() {
@Composable
fun ScriptSettings(script: ModuleInfo) {
var settingsError by remember {
mutableStateOf(null as Throwable?)
}
val settingsInterface = remember {
val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null
runCatching {
(module.getBinding(InterfaceManager::class))?.buildInterface("settings")
}.onFailure {
settingsError = it
}.getOrNull()
(module.getBinding(InterfaceManager::class))?.buildInterface(EnumScriptInterface.SETTINGS)
}
if (settingsInterface == null) {
Text(
text = settingsError?.message ?: "This module does not have any settings",
text = "This module does not have any settings",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(8.dp)
)

View File

@ -703,6 +703,10 @@
"name": "Auto Reload",
"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": {
"name": "Disable Log Anonymization",
"description": "Disables the anonymization of logs"

View File

@ -7,5 +7,6 @@ class Scripting : ConfigContainer() {
val developerMode = boolean("developer_mode", false) { requireRestart() }
val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER); requireRestart() }
val autoReload = unique("auto_reload", "snapchat_only", "all")
val integratedUI = boolean("integrated_ui", 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.type.ModuleInfo
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.NativeJavaObject
import org.mozilla.javascript.ScriptableObject
@ -53,6 +54,7 @@ class JSModule(
registerBindings(
JavaInterfaces(),
InterfaceManager(),
)
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.BindingSide
import me.rhunk.snapenhance.common.scripting.ktx.contextScope
import me.rhunk.snapenhance.scripting.impl.ui.components.Node
import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.RowColumnNode
import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject
import me.rhunk.snapenhance.common.scripting.ui.components.Node
import me.rhunk.snapenhance.common.scripting.ui.components.NodeType
import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode
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.annotations.JSFunction
@ -73,24 +74,36 @@ class InterfaceBuilder {
class InterfaceManager : AbstractBinding("interface-manager", BindingSide.MANAGER) {
private val interfaces = mutableMapOf<String, () -> InterfaceBuilder?>()
class InterfaceManager : AbstractBinding("interface-manager", BindingSide.COMMON) {
private val interfaces = mutableMapOf<String, (args: Map<String, Any?>) -> InterfaceBuilder?>()
fun buildInterface(name: String): InterfaceBuilder? {
return interfaces[name]?.invoke()
fun buildInterface(scriptInterface: EnumScriptInterface, args: Map<String, Any?> = emptyMap()): InterfaceBuilder? {
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() {
interfaces.clear()
}
fun hasInterface(scriptInterfaces: EnumScriptInterface): Boolean {
return interfaces.containsKey(scriptInterfaces.key)
}
@Suppress("unused")
@JSFunction fun create(name: String, callback: Function) {
interfaces[name] = {
interfaces[name] = { args ->
val interfaceBuilder = InterfaceBuilder()
runCatching {
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
}.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.material3.OutlinedButton
@ -13,11 +13,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.scripting.impl.ui.InterfaceBuilder
import me.rhunk.snapenhance.scripting.impl.ui.components.Node
import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType
import me.rhunk.snapenhance.common.scripting.ui.components.Node
import me.rhunk.snapenhance.common.scripting.ui.components.NodeType
import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionNode
import me.rhunk.snapenhance.common.scripting.ui.components.impl.ActionType
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(
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 {
ROW,
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.ui.Alignment
import me.rhunk.snapenhance.scripting.impl.ui.components.Node
import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType
import me.rhunk.snapenhance.common.scripting.ui.components.Node
import me.rhunk.snapenhance.common.scripting.ui.components.NodeType
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.logger.CoreLogger
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.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.view.Gravity
@ -6,12 +6,13 @@ import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ScrollView
import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.features.impl.messaging.Messaging
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 java.lang.reflect.Modifier
import kotlin.reflect.KClass
@ -27,14 +28,18 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
@SuppressLint("ResourceType")
override fun asyncOnActivityCreate() {
menuMap[OperaContextActionMenu::class] = OperaContextActionMenu()
menuMap[OperaDownloadIconMenu::class] = OperaDownloadIconMenu()
menuMap[SettingsGearInjector::class] = SettingsGearInjector()
menuMap[FriendFeedInfoMenu::class] = FriendFeedInfoMenu()
menuMap[ChatActionMenu::class] = ChatActionMenu()
menuMap[SettingsMenu::class] = SettingsMenu()
menuMap.values.forEach { it.context = context; it.init() }
arrayOf(
OperaContextActionMenu(),
OperaDownloadIconMenu(),
SettingsGearInjector(),
FriendFeedInfoMenu(),
ChatActionMenu(),
SettingsMenu()
).forEach {
menuMap[it::class] = it.also {
it.context = context; it.init()
}
}
val messaging = context.feature(Messaging::class)
@ -90,26 +95,29 @@ class MenuViewInjector : Feature("MenuViewInjector", loadParams = FeatureLoadPar
})
}
val viewList = mutableListOf<View>()
context.runOnUiThread {
menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view ->
view.layoutParams = LinearLayout.LayoutParams(
injectedLayout.addView(ScrollView(injectedLayout.context).apply {
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 5, 0, 5)
weight = 1f;
setMargins(0, 100, 0, 0)
}
viewList.add(view)
}
viewList.add(View(injectedLayout.context).apply {
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
30
)
})
viewList.reversed().forEach { injectedLayout.addView(it, 0) }
addView(LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
menuMap[FriendFeedInfoMenu::class]?.inject(event.parent, injectedLayout) { view ->
view.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 5, 0, 5)
}
addView(view)
}
})
}, 0)
}
event.view = injectedLayout

View File

@ -9,8 +9,10 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CompoundButton
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.Switch
import androidx.compose.runtime.remember
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
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.FriendInfo
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.snap.BitmojiSelfie
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)
})
})
}
}
}
}