feat(ui): script list
- add module state - add onSnapActivity and onManagerActivity test functions
This commit is contained in:
@@ -11,6 +11,7 @@ import me.rhunk.snapenhance.core.util.SQLiteDatabaseHelper
|
||||
import me.rhunk.snapenhance.core.util.ktx.getInteger
|
||||
import me.rhunk.snapenhance.core.util.ktx.getLongOrNull
|
||||
import me.rhunk.snapenhance.core.util.ktx.getStringOrNull
|
||||
import me.rhunk.snapenhance.scripting.type.ModuleInfo
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
@@ -60,17 +61,12 @@ class ModDatabase(
|
||||
"expirationTimestamp BIGINT",
|
||||
"length INTEGER"
|
||||
),
|
||||
"analytics_config" to listOf(
|
||||
"userId VARCHAR PRIMARY KEY",
|
||||
"modes VARCHAR"
|
||||
),
|
||||
"analytics" to listOf(
|
||||
"hash VARCHAR PRIMARY KEY",
|
||||
"userId VARCHAR",
|
||||
"conversationId VARCHAR",
|
||||
"timestamp BIGINT",
|
||||
"eventName VARCHAR",
|
||||
"eventData VARCHAR"
|
||||
"scripts" to listOf(
|
||||
"name VARCHAR PRIMARY KEY",
|
||||
"version VARCHAR NOT NULL",
|
||||
"description VARCHAR",
|
||||
"author VARCHAR NOT NULL",
|
||||
"enabled BOOLEAN"
|
||||
)
|
||||
))
|
||||
}
|
||||
@@ -251,4 +247,64 @@ class ModDatabase(
|
||||
ruleIds
|
||||
}
|
||||
}
|
||||
|
||||
fun getScripts(): List<ModuleInfo> {
|
||||
return database.rawQuery("SELECT * FROM scripts", null).use { cursor ->
|
||||
val scripts = mutableListOf<ModuleInfo>()
|
||||
while (cursor.moveToNext()) {
|
||||
scripts.add(
|
||||
ModuleInfo(
|
||||
name = cursor.getStringOrNull("name")!!,
|
||||
version = cursor.getStringOrNull("version")!!,
|
||||
description = cursor.getStringOrNull("description"),
|
||||
author = cursor.getStringOrNull("author"),
|
||||
grantPermissions = null
|
||||
)
|
||||
)
|
||||
}
|
||||
scripts
|
||||
}
|
||||
}
|
||||
|
||||
fun setScriptEnabled(name: String, enabled: Boolean) {
|
||||
executeAsync {
|
||||
database.execSQL("UPDATE scripts SET enabled = ? WHERE name = ?", arrayOf(
|
||||
if (enabled) 1 else 0,
|
||||
name
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun isScriptEnabled(name: String): Boolean {
|
||||
return database.rawQuery("SELECT enabled FROM scripts WHERE name = ?", arrayOf(name)).use { cursor ->
|
||||
if (!cursor.moveToFirst()) return@use false
|
||||
cursor.getInteger("enabled") == 1
|
||||
}
|
||||
}
|
||||
|
||||
fun syncScripts(availableScripts: List<ModuleInfo>) {
|
||||
executeAsync {
|
||||
val enabledScripts = getScripts()
|
||||
val enabledScriptPaths = enabledScripts.map { it.name }
|
||||
val availableScriptPaths = availableScripts.map { it.name }
|
||||
|
||||
enabledScripts.forEach { script ->
|
||||
if (!availableScriptPaths.contains(script.name)) {
|
||||
database.execSQL("DELETE FROM scripts WHERE name = ?", arrayOf(script.name))
|
||||
}
|
||||
}
|
||||
|
||||
availableScripts.forEach { script ->
|
||||
if (!enabledScriptPaths.contains(script.name)) {
|
||||
database.execSQL("INSERT OR REPLACE INTO scripts (name, version, description, author, enabled) VALUES (?, ?, ?, ?, ?)", arrayOf(
|
||||
script.name,
|
||||
script.version,
|
||||
script.description,
|
||||
script.author,
|
||||
0
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,36 +5,63 @@ import androidx.documentfile.provider.DocumentFile
|
||||
import me.rhunk.snapenhance.RemoteSideContext
|
||||
import me.rhunk.snapenhance.bridge.scripting.IScripting
|
||||
import me.rhunk.snapenhance.bridge.scripting.ReloadListener
|
||||
import me.rhunk.snapenhance.scripting.type.ModuleInfo
|
||||
import java.io.InputStream
|
||||
|
||||
class RemoteScriptManager(
|
||||
private val context: RemoteSideContext,
|
||||
) : IScripting.Stub() {
|
||||
private val scriptRuntime = ScriptRuntime(context.log)
|
||||
val runtime = ScriptRuntime(context.log)
|
||||
|
||||
private fun getScriptFolder()
|
||||
= DocumentFile.fromTreeUri(context.androidContext, Uri.parse(context.config.root.scripting.moduleFolder.get()))
|
||||
private fun hasHotReload() = context.config.root.scripting.hotReload.get()
|
||||
|
||||
//private fun hasHotReload() = context.config.root.scripting.hotReload.get()
|
||||
private val reloadListeners = mutableListOf<ReloadListener>()
|
||||
|
||||
private val cachedModuleInfo = mutableMapOf<String, ModuleInfo>()
|
||||
|
||||
fun sync() {
|
||||
getScriptFileNames().forEach { name ->
|
||||
runCatching {
|
||||
getScriptInputStream(name) { stream ->
|
||||
runtime.getModuleInfo(stream!!).also { info ->
|
||||
cachedModuleInfo[name] = info
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
context.log.error("Failed to load module info for $name", it)
|
||||
}
|
||||
}
|
||||
|
||||
context.modDatabase.syncScripts(cachedModuleInfo.values.toList())
|
||||
}
|
||||
|
||||
fun init() {
|
||||
enabledScriptPaths.forEach { path ->
|
||||
val content = getScriptContent(path)
|
||||
scriptRuntime.load(path, content)
|
||||
sync()
|
||||
|
||||
enabledScripts.forEach { path ->
|
||||
val content = getScriptContent(path) ?: return@forEach
|
||||
runtime.load(path, content)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEnabledScriptPaths(): List<String> {
|
||||
val folder = getScriptFolder() ?: return emptyList()
|
||||
return folder.listFiles().filter { it.name?.endsWith(".js") ?: false }.map { it.name!! }
|
||||
private fun <R> getScriptInputStream(name: String, callback: (InputStream?) -> R): R {
|
||||
val file = getScriptFolder()?.findFile(name) ?: return callback(null)
|
||||
return context.androidContext.contentResolver.openInputStream(file.uri)?.use(callback) ?: callback(null)
|
||||
}
|
||||
|
||||
override fun getScriptContent(path: String): String {
|
||||
val folder = getScriptFolder() ?: return ""
|
||||
val file = folder.findFile(path) ?: return ""
|
||||
return context.androidContext.contentResolver.openInputStream(file.uri)?.use {
|
||||
it.readBytes().toString(Charsets.UTF_8)
|
||||
} ?: ""
|
||||
private fun getScriptFileNames(): List<String> {
|
||||
return (getScriptFolder() ?: return emptyList()).listFiles().filter { it.name?.endsWith(".js") ?: false }.map { it.name!! }
|
||||
}
|
||||
|
||||
override fun getEnabledScripts(): List<String> {
|
||||
return getScriptFileNames().filter {
|
||||
context.modDatabase.isScriptEnabled(cachedModuleInfo[it]?.name ?: return@filter false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getScriptContent(name: String): String? {
|
||||
return getScriptInputStream(name) { it?.bufferedReader()?.readText() }
|
||||
}
|
||||
|
||||
override fun registerReloadListener(listener: ReloadListener) {
|
||||
|
||||
@@ -46,6 +46,10 @@ class MainActivity : ComponentActivity() {
|
||||
checkForRequirements()
|
||||
}
|
||||
|
||||
managerContext.scriptManager.runtime.eachModule {
|
||||
callOnManagerLoad(this@MainActivity)
|
||||
}
|
||||
|
||||
sections = EnumSection.values().toList().associateWith {
|
||||
it.section.constructors.first().call()
|
||||
}.onEach { (section, instance) ->
|
||||
|
||||
@@ -17,6 +17,7 @@ import me.rhunk.snapenhance.ui.manager.sections.NotImplemented
|
||||
import me.rhunk.snapenhance.ui.manager.sections.downloads.DownloadsSection
|
||||
import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection
|
||||
import me.rhunk.snapenhance.ui.manager.sections.home.HomeSection
|
||||
import me.rhunk.snapenhance.ui.manager.sections.scripting.ScriptsSection
|
||||
import me.rhunk.snapenhance.ui.manager.sections.social.SocialSection
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@@ -47,7 +48,8 @@ enum class EnumSection(
|
||||
),
|
||||
SCRIPTS(
|
||||
route = "scripts",
|
||||
icon = Icons.Filled.DataObject
|
||||
icon = Icons.Filled.DataObject,
|
||||
section = ScriptsSection::class
|
||||
);
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package me.rhunk.snapenhance.ui.manager.sections.scripting
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import me.rhunk.snapenhance.scripting.type.ModuleInfo
|
||||
import me.rhunk.snapenhance.ui.manager.Section
|
||||
|
||||
class ScriptsSection : Section() {
|
||||
@Composable
|
||||
fun ModuleItem(script: ModuleInfo) {
|
||||
var enabled by remember {
|
||||
mutableStateOf(context.modDatabase.isScriptEnabled(script.name))
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
elevation = CardDefaults.cardElevation()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = script.name,
|
||||
fontSize = 20.sp,
|
||||
)
|
||||
Text(
|
||||
text = script.description ?: "No description",
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = enabled,
|
||||
onCheckedChange = {
|
||||
context.modDatabase.setScriptEnabled(script.name, it)
|
||||
enabled = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val scriptModules = remember {
|
||||
context.modDatabase.getScripts()
|
||||
}
|
||||
|
||||
LazyColumn {
|
||||
item {
|
||||
if (scriptModules.isEmpty()) {
|
||||
Text(
|
||||
text = "No scripts found",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
items(scriptModules.size) { index ->
|
||||
ModuleItem(scriptModules[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ package me.rhunk.snapenhance.bridge.scripting;
|
||||
import me.rhunk.snapenhance.bridge.scripting.ReloadListener;
|
||||
|
||||
interface IScripting {
|
||||
List<String> getEnabledScriptPaths();
|
||||
List<String> getEnabledScripts();
|
||||
|
||||
String getScriptContent(String path);
|
||||
@nullable String getScriptContent(String path);
|
||||
|
||||
void registerReloadListener(ReloadListener listener);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ class SnapEnhance {
|
||||
}
|
||||
})
|
||||
|
||||
enabledScriptPaths.forEach { path ->
|
||||
enabledScripts.forEach { path ->
|
||||
runCatching {
|
||||
scriptRuntime.load(path, getScriptContent(path))
|
||||
}.onFailure {
|
||||
@@ -143,6 +143,7 @@ class SnapEnhance {
|
||||
with(appContext) {
|
||||
features.onActivityCreate()
|
||||
actionManager.init()
|
||||
scriptRuntime.eachModule { callOnSnapActivity(mainActivity!!) }
|
||||
}
|
||||
}.also { time ->
|
||||
appContext.log.verbose("onActivityCreate took $time")
|
||||
|
||||
@@ -1,53 +1,72 @@
|
||||
package me.rhunk.snapenhance.scripting
|
||||
|
||||
import android.app.Activity
|
||||
import me.rhunk.snapenhance.core.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.scripting.type.ModuleInfo
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.FunctionObject
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
import org.mozilla.javascript.Undefined
|
||||
|
||||
class JSModule(
|
||||
val moduleInfo: ModuleInfo,
|
||||
val content: String,
|
||||
) {
|
||||
lateinit var logger: AbstractLogger
|
||||
private lateinit var scope: ScriptableObject
|
||||
private lateinit var moduleObject: ScriptableObject
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun logDebug(message: String) {
|
||||
println(message)
|
||||
fun load() {
|
||||
contextScope {
|
||||
moduleObject = initSafeStandardObjects()
|
||||
moduleObject.putConst("module", moduleObject, buildScriptableObject {
|
||||
putConst("info", this, buildScriptableObject {
|
||||
putConst("name", this, moduleInfo.name)
|
||||
putConst("version", this, moduleInfo.version)
|
||||
putConst("description", this, moduleInfo.description)
|
||||
putConst("author", this, moduleInfo.author)
|
||||
putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion)
|
||||
putConst("minSEVersion", this, moduleInfo.minSEVersion)
|
||||
putConst("grantPermissions", this, moduleInfo.grantPermissions)
|
||||
})
|
||||
})
|
||||
|
||||
moduleObject.putFunction("logInfo") { args ->
|
||||
logger.info(args?.getOrNull(0)?.toString() ?: "null")
|
||||
Undefined.instance
|
||||
}
|
||||
|
||||
evaluateString(moduleObject, content, moduleInfo.name, 1, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun load() {
|
||||
val context = Context.enter()
|
||||
context.optimizationLevel = -1
|
||||
scope = context.initSafeStandardObjects()
|
||||
scope.putConst("module", scope, moduleInfo)
|
||||
|
||||
scope.putConst("logDebug", scope,
|
||||
FunctionObject("logDebug", JSModule::class.java.getDeclaredMethod("logDebug", String::class.java), scope)
|
||||
)
|
||||
|
||||
context.evaluateString(scope, content, moduleInfo.name, 1, null)
|
||||
}
|
||||
|
||||
fun unload() {
|
||||
val context = Context.enter()
|
||||
context.evaluateString(scope, "if (typeof module.onUnload === 'function') module.onUnload();", "onUnload", 1, null)
|
||||
Context.exit()
|
||||
contextScope {
|
||||
moduleObject.scriptable("module")?.function("onUnload")?.call(
|
||||
this,
|
||||
moduleObject,
|
||||
moduleObject,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun callOnCoreLoad() {
|
||||
val context = Context.enter()
|
||||
context.evaluateString(scope, "if (typeof module.onCoreLoad === 'function') module.onCoreLoad();", "onCoreLoad", 1, null)
|
||||
Context.exit()
|
||||
fun callOnSnapActivity(activity: Activity) {
|
||||
contextScope {
|
||||
moduleObject.scriptable("module")?.function("onSnapActivity")?.call(
|
||||
this,
|
||||
moduleObject,
|
||||
moduleObject,
|
||||
arrayOf(activity)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun callOnManagerLoad() {
|
||||
val context = Context.enter()
|
||||
context.evaluateString(scope, "if (typeof module.onManagerLoad === 'function') module.onManagerLoad();", "onManagerLoad", 1, null)
|
||||
Context.exit()
|
||||
fun callOnManagerLoad(activity: Activity) {
|
||||
contextScope {
|
||||
moduleObject.scriptable("module")?.function("onManagerActivity")?.call(
|
||||
this,
|
||||
moduleObject,
|
||||
moduleObject,
|
||||
arrayOf(activity)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package me.rhunk.snapenhance.scripting
|
||||
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.Function
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
|
||||
fun contextScope(f: Context.() -> Unit) {
|
||||
val context = Context.enter()
|
||||
context.optimizationLevel = -1
|
||||
try {
|
||||
context.f()
|
||||
} finally {
|
||||
Context.exit()
|
||||
}
|
||||
}
|
||||
|
||||
fun Scriptable.scriptable(name: String): Scriptable? {
|
||||
return this.get(name, this) as? Scriptable
|
||||
}
|
||||
|
||||
fun Scriptable.function(name: String): Function? {
|
||||
return this.get(name, this) as? Function
|
||||
}
|
||||
|
||||
fun ScriptableObject.putFunction(name: String, proxy: Scriptable.(Array<out Any>?) -> Any) {
|
||||
this.putConst(name, this, object: org.mozilla.javascript.BaseFunction() {
|
||||
override fun call(
|
||||
cx: Context?,
|
||||
scope: Scriptable,
|
||||
thisObj: Scriptable,
|
||||
args: Array<out Any>?
|
||||
): Any {
|
||||
return thisObj.proxy(args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun buildScriptableObject(name: String? = "ScriptableObject", f: ScriptableObject.() -> Unit): ScriptableObject {
|
||||
return object: ScriptableObject() {
|
||||
override fun getClassName() = name
|
||||
}.apply(f)
|
||||
}
|
||||
@@ -4,12 +4,17 @@ import me.rhunk.snapenhance.core.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.scripting.type.ModuleInfo
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
class ScriptRuntime(
|
||||
private val logger: AbstractLogger,
|
||||
) {
|
||||
private val modules = mutableMapOf<String, JSModule>()
|
||||
|
||||
fun eachModule(f: JSModule.() -> Unit) {
|
||||
modules.values.forEach(f)
|
||||
}
|
||||
|
||||
private fun readModuleInfo(reader: BufferedReader): ModuleInfo {
|
||||
val header = reader.readLine()
|
||||
if (!header.startsWith("// ==SE_module==")) {
|
||||
@@ -40,6 +45,10 @@ class ScriptRuntime(
|
||||
)
|
||||
}
|
||||
|
||||
fun getModuleInfo(inputStream: InputStream): ModuleInfo {
|
||||
return readModuleInfo(inputStream.bufferedReader())
|
||||
}
|
||||
|
||||
fun reload(path: String, content: String) {
|
||||
unload(path)
|
||||
load(path, content)
|
||||
|
||||
@@ -15,7 +15,7 @@ class NativeLib {
|
||||
init(classloader)
|
||||
initialized = true
|
||||
}.onFailure {
|
||||
Log.e("SnapEnhance", "NativeLib init failed", it)
|
||||
Log.e("SnapEnhance", "NativeLib init failed")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user