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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user