feat(scripting): module system
This commit is contained in:
@@ -118,7 +118,7 @@ class RemoteSideContext(
|
||||
}
|
||||
|
||||
scriptManager.runtime.eachModule {
|
||||
callFunction("module.onManagerLoad", androidContext)
|
||||
callFunction("module.onSnapEnhanceLoad", androidContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -268,7 +268,7 @@ class ModDatabase(
|
||||
version = cursor.getStringOrNull("version")!!,
|
||||
description = cursor.getStringOrNull("description"),
|
||||
author = cursor.getStringOrNull("author"),
|
||||
grantPermissions = null
|
||||
grantedPermissions = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface
|
||||
import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType
|
||||
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
|
||||
import me.rhunk.snapenhance.scripting.impl.IPCListeners
|
||||
import me.rhunk.snapenhance.scripting.impl.RemoteManagerIPC
|
||||
import me.rhunk.snapenhance.scripting.impl.RemoteScriptConfig
|
||||
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
|
||||
@@ -21,7 +21,9 @@ import kotlin.system.exitProcess
|
||||
class RemoteScriptManager(
|
||||
val context: RemoteSideContext,
|
||||
) : IScripting.Stub() {
|
||||
val runtime = ScriptRuntime(context.androidContext, context.log)
|
||||
val runtime = ScriptRuntime(context.androidContext, context.log).apply {
|
||||
scripting = this@RemoteScriptManager
|
||||
}
|
||||
|
||||
private var autoReloadListener: AutoReloadListener? = null
|
||||
private val autoReloadHandler by lazy {
|
||||
@@ -61,11 +63,11 @@ class RemoteScriptManager(
|
||||
|
||||
fun init() {
|
||||
runtime.buildModuleObject = { module ->
|
||||
module.extras["ipc"] = RemoteManagerIPC(module.moduleInfo, context.log, ipcListeners)
|
||||
module.extras["im"] = InterfaceManager(module.moduleInfo, context.log)
|
||||
module.extras["config"] = RemoteScriptConfig(this@RemoteScriptManager, module.moduleInfo, context.log).also {
|
||||
it.load()
|
||||
}
|
||||
module.registerBindings(
|
||||
ManagerIPC(ipcListeners),
|
||||
InterfaceManager(),
|
||||
ManagerScriptConfig(this@RemoteScriptManager)
|
||||
)
|
||||
}
|
||||
|
||||
sync()
|
||||
@@ -74,12 +76,20 @@ class RemoteScriptManager(
|
||||
}
|
||||
}
|
||||
|
||||
fun loadScript(name: String) {
|
||||
val content = getScriptContent(name) ?: return
|
||||
fun getModulePath(name: String): String? {
|
||||
return cachedModuleInfo.entries.find { it.value.name == name }?.key
|
||||
}
|
||||
|
||||
fun loadScript(path: String) {
|
||||
val content = getScriptContent(path) ?: return
|
||||
if (context.config.root.scripting.autoReload.getNullable() != null) {
|
||||
autoReloadHandler.addFile(getScriptsFolder()?.findFile(name) ?: return)
|
||||
autoReloadHandler.addFile(getScriptsFolder()?.findFile(path) ?: return)
|
||||
}
|
||||
runtime.load(name, content)
|
||||
runtime.load(path, content)
|
||||
}
|
||||
|
||||
fun unloadScript(scriptPath: String) {
|
||||
runtime.unload(scriptPath)
|
||||
}
|
||||
|
||||
private fun <R> getScriptInputStream(name: String, callback: (InputStream?) -> R): R {
|
||||
@@ -140,7 +150,7 @@ class RemoteScriptManager(
|
||||
value: String?,
|
||||
save: Boolean
|
||||
): String? {
|
||||
val scriptConfig = runtime.getModuleByName(module ?: return null)?.extras?.get("config") as? ConfigInterface ?: return null.also {
|
||||
val scriptConfig = runtime.getModuleByName(module ?: return null)?.getBinding(ConfigInterface::class) ?: return null.also {
|
||||
context.log.warn("Failed to get config interface for $module")
|
||||
}
|
||||
val transactionType = ConfigTransactionType.fromKey(action)
|
||||
@@ -154,7 +164,7 @@ class RemoteScriptManager(
|
||||
ConfigTransactionType.SET -> set(key ?: return@runCatching null, value, save)
|
||||
ConfigTransactionType.SAVE -> save()
|
||||
ConfigTransactionType.LOAD -> load()
|
||||
ConfigTransactionType.DELETE -> delete()
|
||||
ConfigTransactionType.DELETE -> deleteConfig()
|
||||
else -> {}
|
||||
}
|
||||
null
|
||||
|
||||
@@ -2,17 +2,13 @@ package me.rhunk.snapenhance.scripting.impl
|
||||
|
||||
import android.os.DeadObjectException
|
||||
import me.rhunk.snapenhance.bridge.scripting.IPCListener
|
||||
import me.rhunk.snapenhance.common.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.common.scripting.impl.IPCInterface
|
||||
import me.rhunk.snapenhance.common.scripting.impl.Listener
|
||||
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
typealias IPCListeners = ConcurrentHashMap<String, MutableMap<String, MutableSet<IPCListener>>> // channel, eventName -> listeners
|
||||
|
||||
class RemoteManagerIPC(
|
||||
private val moduleInfo: ModuleInfo,
|
||||
private val logger: AbstractLogger,
|
||||
class ManagerIPC(
|
||||
private val ipcListeners: IPCListeners = ConcurrentHashMap(),
|
||||
) : IPCInterface() {
|
||||
companion object {
|
||||
@@ -20,22 +16,22 @@ class RemoteManagerIPC(
|
||||
}
|
||||
|
||||
override fun on(eventName: String, listener: Listener) {
|
||||
onBroadcast(moduleInfo.name, eventName, listener)
|
||||
onBroadcast(context.moduleInfo.name, eventName, listener)
|
||||
}
|
||||
|
||||
override fun emit(eventName: String, vararg args: String?) {
|
||||
emit(moduleInfo.name, eventName, *args)
|
||||
emit(context.moduleInfo.name, eventName, *args)
|
||||
}
|
||||
|
||||
override fun onBroadcast(channel: String, eventName: String, listener: Listener) {
|
||||
ipcListeners.getOrPut(channel) { mutableMapOf() }.getOrPut(eventName) { mutableSetOf() }.add(object: IPCListener.Stub() {
|
||||
override fun onMessage(args: Array<out String?>) {
|
||||
try {
|
||||
listener(args)
|
||||
listener(args.toList())
|
||||
} catch (doe: DeadObjectException) {
|
||||
ipcListeners[channel]?.get(eventName)?.remove(this)
|
||||
} catch (t: Throwable) {
|
||||
logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG)
|
||||
context.runtime.logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -48,7 +44,7 @@ class RemoteManagerIPC(
|
||||
} catch (doe: DeadObjectException) {
|
||||
ipcListeners[channel]?.get(eventName)?.remove(it)
|
||||
} catch (t: Throwable) {
|
||||
logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG)
|
||||
context.runtime.logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
package me.rhunk.snapenhance.scripting.impl
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import me.rhunk.snapenhance.common.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface
|
||||
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
|
||||
import me.rhunk.snapenhance.scripting.RemoteScriptManager
|
||||
import java.io.File
|
||||
|
||||
class RemoteScriptConfig(
|
||||
private val remoteScriptManager: RemoteScriptManager,
|
||||
moduleInfo: ModuleInfo,
|
||||
private val logger: AbstractLogger,
|
||||
class ManagerScriptConfig(
|
||||
private val remoteScriptManager: RemoteScriptManager
|
||||
) : ConfigInterface() {
|
||||
private val configFile = File(remoteScriptManager.getModuleDataFolder(moduleInfo.name), "config.json")
|
||||
private val configFile by lazy { File(remoteScriptManager.getModuleDataFolder(context.moduleInfo.name), "config.json") }
|
||||
private var config = JsonObject()
|
||||
|
||||
override fun get(key: String, defaultValue: Any?): String? {
|
||||
@@ -46,12 +42,16 @@ class RemoteScriptConfig(
|
||||
}
|
||||
config = remoteScriptManager.context.gson.fromJson(configFile.readText(), JsonObject::class.java)
|
||||
}.onFailure {
|
||||
logger.error("Failed to load config file", it)
|
||||
context.runtime.logger.error("Failed to load config file", it)
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete() {
|
||||
override fun deleteConfig() {
|
||||
configFile.delete()
|
||||
}
|
||||
|
||||
override fun onInit() {
|
||||
load()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package me.rhunk.snapenhance.scripting.impl.ui
|
||||
|
||||
import me.rhunk.snapenhance.common.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
|
||||
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 org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.Function
|
||||
import org.mozilla.javascript.annotations.JSFunction
|
||||
|
||||
@@ -73,27 +73,31 @@ class InterfaceBuilder {
|
||||
|
||||
|
||||
|
||||
class InterfaceManager(
|
||||
private val moduleInfo: ModuleInfo,
|
||||
private val logger: AbstractLogger
|
||||
) {
|
||||
class InterfaceManager : AbstractBinding("interface-manager", BindingSide.MANAGER) {
|
||||
private val interfaces = mutableMapOf<String, () -> InterfaceBuilder?>()
|
||||
|
||||
fun buildInterface(name: String): InterfaceBuilder? {
|
||||
return interfaces[name]?.invoke()
|
||||
}
|
||||
|
||||
override fun onDispose() {
|
||||
interfaces.clear()
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JSFunction fun create(name: String, callback: Function) {
|
||||
interfaces[name] = {
|
||||
val interfaceBuilder = InterfaceBuilder()
|
||||
runCatching {
|
||||
Context.enter()
|
||||
callback.call(Context.getCurrentContext(), callback, callback, arrayOf(interfaceBuilder))
|
||||
Context.exit()
|
||||
contextScope {
|
||||
callback.call(this, callback, callback, arrayOf(interfaceBuilder))
|
||||
}
|
||||
interfaceBuilder
|
||||
}.onFailure {
|
||||
logger.error("Failed to create interface $name for ${moduleInfo.name}", it)
|
||||
context.runtime.logger.error("Failed to create interface $name for ${context.moduleInfo.name}", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getObject() = this
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
@@ -14,6 +15,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -70,12 +72,27 @@ class ScriptsSection : Section() {
|
||||
}
|
||||
Switch(
|
||||
checked = enabled,
|
||||
onCheckedChange = {
|
||||
context.modDatabase.setScriptEnabled(script.name, it)
|
||||
if (it) {
|
||||
context.scriptManager.loadScript(script.name)
|
||||
onCheckedChange = { isChecked ->
|
||||
context.modDatabase.setScriptEnabled(script.name, isChecked)
|
||||
enabled = isChecked
|
||||
runCatching {
|
||||
val modulePath = context.scriptManager.getModulePath(script.name)!!
|
||||
context.scriptManager.unloadScript(modulePath)
|
||||
if (isChecked) {
|
||||
context.scriptManager.loadScript(modulePath)
|
||||
context.scriptManager.runtime.getModuleByName(script.name)
|
||||
?.callFunction("module.onSnapEnhanceLoad")
|
||||
context.shortToast("Loaded script ${script.name}")
|
||||
} else {
|
||||
context.shortToast("Unloaded script ${script.name}")
|
||||
}
|
||||
}.onFailure { throwable ->
|
||||
enabled = !isChecked
|
||||
("Failed to ${if (isChecked) "enable" else "disable"} script").let {
|
||||
context.log.error(it, throwable)
|
||||
context.shortToast(it)
|
||||
}
|
||||
}
|
||||
enabled = it
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -130,7 +147,7 @@ class ScriptsSection : Section() {
|
||||
val settingsInterface = remember {
|
||||
val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null
|
||||
runCatching {
|
||||
(module.extras["im"] as? InterfaceManager)?.buildInterface("settings")
|
||||
(module.getBinding(InterfaceManager::class))?.buildInterface("settings")
|
||||
}.onFailure {
|
||||
settingsError = it
|
||||
}.getOrNull()
|
||||
@@ -228,4 +245,18 @@ class ScriptsSection : Section() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun TopBarActions(rowScope: RowScope) {
|
||||
rowScope.apply {
|
||||
IconButton(onClick = {
|
||||
context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply {
|
||||
data = "https://github.com/SnapEnhance/docs".toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
})
|
||||
}) {
|
||||
Icon(imageVector = Icons.Default.LibraryBooks, contentDescription = "Documentation")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user