mirror of
https://github.com/fromchat-messenger/app.git
synced 2026-09-22 19:15:05 +03:00
Redesign plugins UI and demo raw + high-level hooks
- Match exteraGram-style card layout with master toggle and plugin actions - Add usage manifest field, pin/delete/removePlugin engine APIs - Hello World plugin hooks Logger.d (raw) and send pipeline (high-level) - Bundle updated hello_world.fcplugin; stop debug auto-enable Co-authored-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package ru.fromchat.plugins.integration
|
||||||
|
|
||||||
|
actual object PluginInstallSupport {
|
||||||
|
actual fun installFromPicker(): Boolean = false
|
||||||
|
}
|
||||||
@@ -340,14 +340,24 @@
|
|||||||
<string name="debug_tools_d">Просмотр ответов API профиля и личных сообщений.</string>
|
<string name="debug_tools_d">Просмотр ответов API профиля и личных сообщений.</string>
|
||||||
|
|
||||||
<string name="plugins_title">Плагины</string>
|
<string name="plugins_title">Плагины</string>
|
||||||
|
<string name="plugins_enable_system">Включить систему плагинов</string>
|
||||||
<string name="plugins_engine">Движок плагинов</string>
|
<string name="plugins_engine">Движок плагинов</string>
|
||||||
<string name="plugins_engine_d">Запуск Kotlin-плагинов для расширения FromChat.</string>
|
<string name="plugins_engine_d">Запуск Kotlin-плагинов для расширения FromChat.</string>
|
||||||
<string name="plugins_developer_mode">Режим разработчика</string>
|
<string name="plugins_developer_mode">Режим разработчика</string>
|
||||||
<string name="plugins_developer_mode_d">Включает локальный DevServer на порту 42690.</string>
|
<string name="plugins_developer_mode_d">Включает локальный DevServer на порту 42690.</string>
|
||||||
<string name="plugins_installed">Установленные плагины</string>
|
<string name="plugins_installed">Установленные плагины</string>
|
||||||
<string name="plugins_none_installed">Плагины ещё не установлены.</string>
|
<string name="plugins_none_installed">Плагины ещё не установлены.</string>
|
||||||
|
<string name="plugins_no_results">Нет плагинов по вашему запросу.</string>
|
||||||
<string name="plugins_enabled">Включено</string>
|
<string name="plugins_enabled">Включено</string>
|
||||||
<string name="plugins_settings_title">Настройки плагина</string>
|
<string name="plugins_settings_title">Настройки плагина</string>
|
||||||
|
<string name="plugins_search">Поиск плагинов</string>
|
||||||
|
<string name="plugins_usage">Использование:</string>
|
||||||
|
<string name="plugins_version_author">Версия %1$s • @%2$s</string>
|
||||||
|
<string name="plugins_delete">Удалить</string>
|
||||||
|
<string name="plugins_delete_confirm">Удалить «%1$s» и все его данные?</string>
|
||||||
|
<string name="plugins_share_copied">Информация о плагине скопирована.</string>
|
||||||
|
<string name="plugins_info_title">О плагинах</string>
|
||||||
|
<string name="plugins_info_body">Плагины могут менять интерфейс, перехватывать сообщения и подключаться к внутренним методам приложения. Устанавливайте плагины только из проверенных источников.</string>
|
||||||
|
|
||||||
<string name="settings_category_appearance">Оформление</string>
|
<string name="settings_category_appearance">Оформление</string>
|
||||||
<string name="settings_category_appearance_d">Тема и Material You</string>
|
<string name="settings_category_appearance_d">Тема и Material You</string>
|
||||||
|
|||||||
@@ -371,14 +371,24 @@
|
|||||||
<string name="debug_tools_d">Inspect profile and DM endpoints used by the client.</string>
|
<string name="debug_tools_d">Inspect profile and DM endpoints used by the client.</string>
|
||||||
|
|
||||||
<string name="plugins_title">Plugins</string>
|
<string name="plugins_title">Plugins</string>
|
||||||
|
<string name="plugins_enable_system">Enable plugin system</string>
|
||||||
<string name="plugins_engine">Plugin engine</string>
|
<string name="plugins_engine">Plugin engine</string>
|
||||||
<string name="plugins_engine_d">Run Kotlin plugins that extend FromChat.</string>
|
<string name="plugins_engine_d">Run Kotlin plugins that extend FromChat.</string>
|
||||||
<string name="plugins_developer_mode">Developer mode</string>
|
<string name="plugins_developer_mode">Developer mode</string>
|
||||||
<string name="plugins_developer_mode_d">Enable the local DevServer on port 42690.</string>
|
<string name="plugins_developer_mode_d">Enable the local DevServer on port 42690.</string>
|
||||||
<string name="plugins_installed">Installed plugins</string>
|
<string name="plugins_installed">Installed plugins</string>
|
||||||
<string name="plugins_none_installed">No plugins installed yet.</string>
|
<string name="plugins_none_installed">No plugins installed yet.</string>
|
||||||
|
<string name="plugins_no_results">No plugins match your search.</string>
|
||||||
<string name="plugins_enabled">Enabled</string>
|
<string name="plugins_enabled">Enabled</string>
|
||||||
<string name="plugins_settings_title">Plugin settings</string>
|
<string name="plugins_settings_title">Plugin settings</string>
|
||||||
|
<string name="plugins_search">Search plugins</string>
|
||||||
|
<string name="plugins_usage">Usage:</string>
|
||||||
|
<string name="plugins_version_author">Version %1$s • @%2$s</string>
|
||||||
|
<string name="plugins_delete">Delete</string>
|
||||||
|
<string name="plugins_delete_confirm">Remove \"%1$s\" and all its data?</string>
|
||||||
|
<string name="plugins_share_copied">Plugin info copied to clipboard.</string>
|
||||||
|
<string name="plugins_info_title">About plugins</string>
|
||||||
|
<string name="plugins_info_body">Plugins can modify UI, intercept messages, and hook internal app methods. Only install plugins from sources you trust.</string>
|
||||||
|
|
||||||
<!-- Settings categories -->
|
<!-- Settings categories -->
|
||||||
<string name="settings_category_appearance">Appearance</string>
|
<string name="settings_category_appearance">Appearance</string>
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package ru.fromchat.plugins.integration
|
||||||
|
|
||||||
|
expect object PluginInstallSupport {
|
||||||
|
fun installFromPicker(): Boolean
|
||||||
|
}
|
||||||
@@ -1,80 +1,126 @@
|
|||||||
package ru.fromchat.ui.main.settings
|
package ru.fromchat.ui.main.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Extension
|
import androidx.compose.material.icons.filled.Extension
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.OpenInNew
|
||||||
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.PushPin
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MediumTopAppBar
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.pr0gramm3r101.components.Category
|
import com.pr0gramm3r101.components.Category
|
||||||
import com.pr0gramm3r101.components.ListItem
|
import com.pr0gramm3r101.components.ListItem
|
||||||
import com.pr0gramm3r101.components.SwitchListItem
|
import com.pr0gramm3r101.components.SwitchListItem
|
||||||
|
import com.pr0gramm3r101.utils.supportClipboardManagerImpl
|
||||||
import com.pr0gramm3r101.utils.verticalScroll
|
import com.pr0gramm3r101.utils.verticalScroll
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import ru.fromchat.Res
|
import ru.fromchat.Res
|
||||||
|
import ru.fromchat.back
|
||||||
|
import ru.fromchat.cancel
|
||||||
|
import ru.fromchat.plugins.PluginManifest
|
||||||
import ru.fromchat.plugins.PluginSetting
|
import ru.fromchat.plugins.PluginSetting
|
||||||
import ru.fromchat.plugins.host.PluginEngine
|
import ru.fromchat.plugins.host.PluginEngine
|
||||||
|
import ru.fromchat.plugins.integration.PluginInstallSupport
|
||||||
import ru.fromchat.ui.LocalNavController
|
import ru.fromchat.ui.LocalNavController
|
||||||
import ru.fromchat.ui.components.Text
|
import ru.fromchat.ui.components.Text
|
||||||
import ru.fromchat.plugins_title
|
import ru.fromchat.plugins_delete
|
||||||
import ru.fromchat.plugins_engine
|
import ru.fromchat.plugins_delete_confirm
|
||||||
import ru.fromchat.plugins_engine_d
|
|
||||||
import ru.fromchat.plugins_developer_mode
|
import ru.fromchat.plugins_developer_mode
|
||||||
import ru.fromchat.plugins_developer_mode_d
|
import ru.fromchat.plugins_developer_mode_d
|
||||||
import ru.fromchat.plugins_installed
|
import ru.fromchat.plugins_enable_system
|
||||||
|
import ru.fromchat.plugins_engine_d
|
||||||
|
import ru.fromchat.plugins_info_body
|
||||||
|
import ru.fromchat.plugins_info_title
|
||||||
|
import ru.fromchat.plugins_no_results
|
||||||
import ru.fromchat.plugins_none_installed
|
import ru.fromchat.plugins_none_installed
|
||||||
|
import ru.fromchat.plugins_search
|
||||||
import ru.fromchat.plugins_settings_title
|
import ru.fromchat.plugins_settings_title
|
||||||
|
import ru.fromchat.plugins_share_copied
|
||||||
|
import ru.fromchat.plugins_title
|
||||||
|
import ru.fromchat.plugins_usage
|
||||||
|
import ru.fromchat.plugins_version_author
|
||||||
|
|
||||||
|
private val PluginMasterBlue = Color(0xFF3D85C6)
|
||||||
|
private val PluginCardBackground = Color(0xFF1A1A1A)
|
||||||
|
private val PluginDestructiveRed = Color(0xFFD9534F)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PluginsScreen() {
|
fun PluginsScreen() {
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val clipboard = supportClipboardManagerImpl
|
||||||
val engineEnabled by PluginEngine.engineEnabled.collectAsState()
|
val engineEnabled by PluginEngine.engineEnabled.collectAsState()
|
||||||
val developerMode by PluginEngine.developerMode.collectAsState()
|
val developerMode by PluginEngine.developerMode.collectAsState()
|
||||||
val installed by PluginEngine.installed.collectAsState()
|
val installed by PluginEngine.installed.collectAsState()
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
var showInfoDialog by remember { mutableStateOf(false) }
|
||||||
|
var deleteTarget by remember { mutableStateOf<PluginManifest?>(null) }
|
||||||
|
var shareNotice by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Scaffold(
|
val filtered = remember(installed, searchQuery) {
|
||||||
modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
|
val sorted = PluginEngine.sortedInstalled()
|
||||||
topBar = {
|
if (searchQuery.isBlank()) {
|
||||||
MediumTopAppBar(
|
sorted
|
||||||
title = {
|
} else {
|
||||||
Text(stringResource(Res.string.plugins_title), maxLines = 1, overflow = TextOverflow.Ellipsis)
|
val query = searchQuery.trim().lowercase()
|
||||||
},
|
sorted.filter {
|
||||||
scrollBehavior = scrollBehavior,
|
it.name.lowercase().contains(query) ||
|
||||||
)
|
it.description.lowercase().contains(query) ||
|
||||||
},
|
it.author.lowercase().contains(query) ||
|
||||||
) { innerPadding ->
|
it.id.lowercase().contains(query)
|
||||||
Column(
|
}
|
||||||
Modifier
|
}
|
||||||
.fillMaxSize()
|
}
|
||||||
.verticalScroll()
|
|
||||||
.padding(innerPadding),
|
if (showInfoDialog) {
|
||||||
) {
|
AlertDialog(
|
||||||
Category(Modifier.padding(top = 16.dp)) {
|
onDismissRequest = { showInfoDialog = false },
|
||||||
SwitchListItem(
|
title = { Text(stringResource(Res.string.plugins_info_title)) },
|
||||||
headline = stringResource(Res.string.plugins_engine),
|
text = {
|
||||||
supportingText = stringResource(Res.string.plugins_engine_d),
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
checked = engineEnabled,
|
Text(stringResource(Res.string.plugins_info_body))
|
||||||
onCheckedChange = PluginEngine::setEngineEnabled,
|
Text(stringResource(Res.string.plugins_engine_d))
|
||||||
divider = true,
|
|
||||||
)
|
|
||||||
SwitchListItem(
|
SwitchListItem(
|
||||||
headline = stringResource(Res.string.plugins_developer_mode),
|
headline = stringResource(Res.string.plugins_developer_mode),
|
||||||
supportingText = stringResource(Res.string.plugins_developer_mode_d),
|
supportingText = stringResource(Res.string.plugins_developer_mode_d),
|
||||||
@@ -83,50 +129,339 @@ fun PluginsScreen() {
|
|||||||
enabled = engineEnabled,
|
enabled = engineEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Category(Modifier.padding(top = 8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(Res.string.plugins_installed),
|
|
||||||
modifier = Modifier.padding(horizontal = SettingsStepHorizontalPadding, vertical = 8.dp),
|
|
||||||
)
|
|
||||||
if (installed.isEmpty()) {
|
|
||||||
ListItem(
|
|
||||||
headline = stringResource(Res.string.plugins_none_installed),
|
|
||||||
leadingContent = { Icon(Icons.Default.Extension, contentDescription = null) },
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
installed.forEachIndexed { index, manifest ->
|
|
||||||
SwitchListItem(
|
|
||||||
headline = manifest.name,
|
|
||||||
supportingText = manifest.description.ifBlank { manifest.id },
|
|
||||||
leadingContent = { Icon(Icons.Default.Extension, contentDescription = null) },
|
|
||||||
checked = PluginEngine.isPluginEnabled(manifest.id),
|
|
||||||
onCheckedChange = { enabled ->
|
|
||||||
PluginEngine.setPluginEnabled(manifest.id, enabled)
|
|
||||||
},
|
},
|
||||||
enabled = engineEnabled,
|
confirmButton = {
|
||||||
divider = index < installed.lastIndex,
|
TextButton(onClick = { showInfoDialog = false }) {
|
||||||
listItemOnClick = {
|
Text(stringResource(Res.string.back))
|
||||||
navController.navigate(SettingsRoutes.pluginDetail(manifest.id))
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteTarget?.let { manifest ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { deleteTarget = null },
|
||||||
|
title = { Text(stringResource(Res.string.plugins_delete)) },
|
||||||
|
text = { Text(stringResource(Res.string.plugins_delete_confirm, manifest.name)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
PluginEngine.removePlugin(manifest.id)
|
||||||
|
deleteTarget = null
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(Res.string.plugins_delete))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { deleteTarget = null }) {
|
||||||
|
Text(stringResource(Res.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shareNotice) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { shareNotice = false },
|
||||||
|
text = { Text(stringResource(Res.string.plugins_share_copied)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { shareNotice = false }) {
|
||||||
|
Text(stringResource(Res.string.back))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(Res.string.plugins_title),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.navigateUp() }) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.back))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { /* search toggled via field below */ }) {
|
||||||
|
Icon(Icons.Default.Search, contentDescription = stringResource(Res.string.plugins_search))
|
||||||
|
}
|
||||||
|
IconButton(onClick = { PluginInstallSupport.installFromPicker() }) {
|
||||||
|
Icon(Icons.Outlined.Add, contentDescription = null)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { showInfoDialog = true }) {
|
||||||
|
Icon(Icons.Default.Info, contentDescription = stringResource(Res.string.plugins_info_title))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.Black,
|
||||||
|
titleContentColor = Color.White,
|
||||||
|
navigationIconContentColor = Color.White,
|
||||||
|
actionIconContentColor = Color.White,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = Color.Black,
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll()
|
||||||
|
.padding(innerPadding)
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = { searchQuery = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp, bottom = 12.dp),
|
||||||
|
placeholder = { Text(stringResource(Res.string.plugins_search)) },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
PluginMasterToggle(
|
||||||
|
checked = engineEnabled,
|
||||||
|
onCheckedChange = PluginEngine::setEngineEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
if (filtered.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = if (installed.isEmpty()) {
|
||||||
|
stringResource(Res.string.plugins_none_installed)
|
||||||
|
} else {
|
||||||
|
stringResource(Res.string.plugins_no_results)
|
||||||
|
},
|
||||||
|
color = Color.White.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.padding(vertical = 24.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
filtered.forEach { manifest ->
|
||||||
|
PluginCard(
|
||||||
|
manifest = manifest,
|
||||||
|
engineEnabled = engineEnabled,
|
||||||
|
pinned = PluginEngine.isPluginPinned(manifest.id),
|
||||||
|
enabled = PluginEngine.isPluginEnabled(manifest.id),
|
||||||
|
onEnabledChange = { PluginEngine.setPluginEnabled(manifest.id, it) },
|
||||||
|
onOpenSettings = {
|
||||||
|
navController.navigate(SettingsRoutes.pluginDetail(manifest.id))
|
||||||
|
},
|
||||||
|
onShare = {
|
||||||
|
scope.launch {
|
||||||
|
clipboard.setText("${manifest.name} (${manifest.id}) v${manifest.version}")
|
||||||
|
shareNotice = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTogglePin = {
|
||||||
|
PluginEngine.setPluginPinned(manifest.id, !PluginEngine.isPluginPinned(manifest.id))
|
||||||
|
},
|
||||||
|
onDelete = { deleteTarget = manifest },
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PluginMasterToggle(
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onCheckedChange(!checked) },
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = PluginMasterBlue,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.plugins_enable_system),
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = Color.White,
|
||||||
|
checkedTrackColor = Color.White.copy(alpha = 0.35f),
|
||||||
|
uncheckedThumbColor = Color.White,
|
||||||
|
uncheckedTrackColor = Color.Black.copy(alpha = 0.25f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PluginCard(
|
||||||
|
manifest: PluginManifest,
|
||||||
|
engineEnabled: Boolean,
|
||||||
|
pinned: Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
onEnabledChange: (Boolean) -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
onShare: () -> Unit,
|
||||||
|
onTogglePin: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = PluginCardBackground,
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Extension,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = PluginMasterBlue,
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
)
|
||||||
|
Switch(
|
||||||
|
checked = enabled,
|
||||||
|
onCheckedChange = onEnabledChange,
|
||||||
|
enabled = engineEnabled,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = PluginMasterBlue,
|
||||||
|
checkedTrackColor = PluginMasterBlue.copy(alpha = 0.45f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = manifest.name,
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
|
||||||
|
val authorLabel = manifest.author.removePrefix("@")
|
||||||
|
if (authorLabel.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
Res.string.plugins_version_author,
|
||||||
|
manifest.version,
|
||||||
|
authorLabel,
|
||||||
|
),
|
||||||
|
color = Color.White.copy(alpha = 0.55f),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = manifest.version,
|
||||||
|
color = Color.White.copy(alpha = 0.55f),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.description.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = manifest.description,
|
||||||
|
color = Color.White.copy(alpha = 0.85f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(top = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val usage = manifest.usage.ifBlank {
|
||||||
|
manifest.description
|
||||||
|
}
|
||||||
|
if (usage.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(Res.string.plugins_usage),
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier.padding(top = 12.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = usage,
|
||||||
|
color = Color.White.copy(alpha = 0.75f),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
PluginCardAction(Icons.Default.Share, onShare)
|
||||||
|
PluginCardAction(Icons.Default.OpenInNew, onOpenSettings)
|
||||||
|
PluginCardAction(
|
||||||
|
imageVector = if (pinned) Icons.Default.PushPin else Icons.Outlined.PushPin,
|
||||||
|
onClick = onTogglePin,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
IconButton(onClick = onDelete) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(Res.string.plugins_delete),
|
||||||
|
tint = PluginDestructiveRed,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PluginCardAction(
|
||||||
|
imageVector: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.White.copy(alpha = 0.85f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PluginDetailScreen(pluginId: String) {
|
fun PluginDetailScreen(pluginId: String) {
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
|
val navController = LocalNavController.current
|
||||||
val plugin = PluginEngine.loadedPlugin(pluginId)
|
val plugin = PluginEngine.loadedPlugin(pluginId)
|
||||||
val settings = plugin?.instance?.createSettings().orEmpty()
|
val settings = plugin?.instance?.createSettings().orEmpty()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
MediumTopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
plugin?.manifest?.name ?: pluginId,
|
plugin?.manifest?.name ?: pluginId,
|
||||||
@@ -134,7 +469,11 @@ fun PluginDetailScreen(pluginId: String) {
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { navController.navigateUp() }) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.back))
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
@@ -189,7 +528,7 @@ fun PluginDetailScreen(pluginId: String) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
androidx.compose.material3.OutlinedTextField(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { value ->
|
onValueChange = { value ->
|
||||||
text = value
|
text = value
|
||||||
@@ -198,7 +537,7 @@ fun PluginDetailScreen(pluginId: String) {
|
|||||||
label = { Text(setting.text) },
|
label = { Text(setting.text) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = SettingsStepHorizontalPadding, vertical = 8.dp)
|
.padding(horizontal = SettingsStepHorizontalPadding, vertical = 8.dp)
|
||||||
.fillMaxSize(),
|
.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package ru.fromchat.plugins.integration
|
||||||
|
|
||||||
|
actual object PluginInstallSupport {
|
||||||
|
actual fun installFromPicker(): Boolean = false
|
||||||
|
}
|
||||||
+6
-8
@@ -17,22 +17,20 @@ object DesktopPluginBootstrap {
|
|||||||
PluginEngine.appVersionProvider = { ru.fromchat.AppBuildInfo.version }
|
PluginEngine.appVersionProvider = { ru.fromchat.AppBuildInfo.version }
|
||||||
installBundledPlugin(classLoader)
|
installBundledPlugin(classLoader)
|
||||||
PluginEngine.init()
|
PluginEngine.init()
|
||||||
if (ru.fromchat.AppBuildInfo.isDebug) {
|
|
||||||
PluginEngine.setEngineEnabled(true)
|
|
||||||
PluginEngine.setPluginEnabled("hello_world", true)
|
|
||||||
}
|
|
||||||
PluginEngine.dispatchAppEvent(AppEvent.START)
|
PluginEngine.dispatchAppEvent(AppEvent.START)
|
||||||
FeatureGate.registerDefault("calls") { ru.fromchat.config.ServerConfig.callsEnabled }
|
FeatureGate.registerDefault("calls") { ru.fromchat.config.ServerConfig.callsEnabled }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun installBundledPlugin(classLoader: ClassLoader) {
|
private fun installBundledPlugin(classLoader: ClassLoader) {
|
||||||
val installed = desktopPluginsRoot().resolve("hello_world")
|
|
||||||
if (installed.exists()) return
|
|
||||||
val stream = classLoader.getResourceAsStream("bundled_plugins/hello_world.fcplugin") ?: return
|
val stream = classLoader.getResourceAsStream("bundled_plugins/hello_world.fcplugin") ?: return
|
||||||
val temp = File.createTempFile("hello_world", ".fcplugin")
|
val temp = File.createTempFile("hello_world", ".fcplugin")
|
||||||
temp.outputStream().use { out -> stream.copyTo(out) }
|
temp.outputStream().use { out -> stream.copyTo(out) }
|
||||||
runCatching { PluginEngine.installFcPluginArchive(temp) }
|
runCatching {
|
||||||
.onFailure { Logger.w("Plugins", "Bundled plugin install failed: ${it.message}") }
|
val bundledDir = temp.parentFile!!.resolve("bundled")
|
||||||
|
bundledDir.mkdirs()
|
||||||
|
temp.copyTo(bundledDir.resolve("hello_world.fcplugin"), overwrite = true)
|
||||||
|
PluginEngine.ensureBundledPlugins(bundledDir)
|
||||||
|
}.onFailure { Logger.w("Plugins", "Bundled plugin install failed: ${it.message}") }
|
||||||
temp.delete()
|
temp.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
package ru.fromchat.plugins.integration
|
||||||
|
|
||||||
|
import ru.fromchat.plugins.host.PluginEngine
|
||||||
|
import java.awt.FileDialog
|
||||||
|
import java.awt.Frame
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
actual object PluginInstallSupport {
|
||||||
|
actual fun installFromPicker(): Boolean {
|
||||||
|
val dialog = FileDialog(null as Frame?, "Install .fcplugin", FileDialog.LOAD).apply {
|
||||||
|
file = "*.fcplugin"
|
||||||
|
isVisible = true
|
||||||
|
}
|
||||||
|
val selected = dialog.file ?: return false
|
||||||
|
val directory = dialog.directory ?: return false
|
||||||
|
val file = File(directory, selected)
|
||||||
|
if (!file.exists() || file.extension.lowercase() != "fcplugin") return false
|
||||||
|
return runCatching {
|
||||||
|
PluginEngine.installFcPluginArchive(file)
|
||||||
|
true
|
||||||
|
}.getOrDefault(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -24,6 +24,7 @@ object PluginEngine : PluginHostBridge {
|
|||||||
val installed: StateFlow<List<PluginManifest>> = _installed.asStateFlow()
|
val installed: StateFlow<List<PluginManifest>> = _installed.asStateFlow()
|
||||||
|
|
||||||
private val enabledIds = linkedSetOf<String>()
|
private val enabledIds = linkedSetOf<String>()
|
||||||
|
private val pinnedIds = linkedSetOf<String>()
|
||||||
private val loaded = mutableMapOf<String, LoadedPlugin>()
|
private val loaded = mutableMapOf<String, LoadedPlugin>()
|
||||||
private val settings = mutableMapOf<String, MutableMap<String, String>>()
|
private val settings = mutableMapOf<String, MutableMap<String, String>>()
|
||||||
private val sharedUnhooks = mutableMapOf<String, MutableList<() -> Unit>>()
|
private val sharedUnhooks = mutableMapOf<String, MutableList<() -> Unit>>()
|
||||||
@@ -72,6 +73,40 @@ object PluginEngine : PluginHostBridge {
|
|||||||
|
|
||||||
fun isPluginEnabled(pluginId: String): Boolean = pluginId in enabledIds
|
fun isPluginEnabled(pluginId: String): Boolean = pluginId in enabledIds
|
||||||
|
|
||||||
|
fun isPluginPinned(pluginId: String): Boolean = pluginId in pinnedIds
|
||||||
|
|
||||||
|
fun setPluginPinned(pluginId: String, pinned: Boolean) {
|
||||||
|
if (pinned) {
|
||||||
|
pinnedIds += pluginId
|
||||||
|
} else {
|
||||||
|
pinnedIds -= pluginId
|
||||||
|
}
|
||||||
|
persistPinnedState()
|
||||||
|
refreshInstalledList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sortedInstalled(): List<PluginManifest> {
|
||||||
|
val manifests = _installed.value
|
||||||
|
return manifests.sortedWith(
|
||||||
|
compareByDescending<PluginManifest> { it.id in pinnedIds }
|
||||||
|
.thenBy { it.name.lowercase() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removePlugin(pluginId: String) {
|
||||||
|
unloadPlugin(pluginId)
|
||||||
|
enabledIds -= pluginId
|
||||||
|
pinnedIds -= pluginId
|
||||||
|
settings.remove(pluginId)
|
||||||
|
val dir = pluginsRootProvider().resolve(pluginId)
|
||||||
|
if (dir.exists()) {
|
||||||
|
dir.deleteRecursively()
|
||||||
|
}
|
||||||
|
persistEnabledState()
|
||||||
|
persistPinnedState()
|
||||||
|
refreshInstalledList()
|
||||||
|
}
|
||||||
|
|
||||||
fun loadedPlugin(pluginId: String): LoadedPlugin? = loaded[pluginId]
|
fun loadedPlugin(pluginId: String): LoadedPlugin? = loaded[pluginId]
|
||||||
|
|
||||||
fun getPluginSettingBoolean(pluginId: String, key: String, default: Boolean = false): Boolean =
|
fun getPluginSettingBoolean(pluginId: String, key: String, default: Boolean = false): Boolean =
|
||||||
@@ -238,12 +273,21 @@ object PluginEngine : PluginHostBridge {
|
|||||||
enabledIds.clear()
|
enabledIds.clear()
|
||||||
enabledIds += enabledFile.readLines().map { it.trim() }.filter { it.isNotEmpty() }
|
enabledIds += enabledFile.readLines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
}
|
}
|
||||||
|
val pinnedFile = pluginsRootProvider().resolve("pinned_plugins.txt")
|
||||||
|
if (pinnedFile.exists()) {
|
||||||
|
pinnedIds.clear()
|
||||||
|
pinnedIds += pinnedFile.readLines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistEnabledState() {
|
private fun persistEnabledState() {
|
||||||
pluginsRootProvider().resolve("enabled_plugins.txt").writeText(enabledIds.joinToString("\n"))
|
pluginsRootProvider().resolve("enabled_plugins.txt").writeText(enabledIds.joinToString("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun persistPinnedState() {
|
||||||
|
pluginsRootProvider().resolve("pinned_plugins.txt").writeText(pinnedIds.joinToString("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun projectZipExtract(archive: File, dest: File) {
|
private fun projectZipExtract(archive: File, dest: File) {
|
||||||
java.util.zip.ZipFile(archive).use { zip ->
|
java.util.zip.ZipFile(archive).use { zip ->
|
||||||
zip.entries().asSequence().forEach { entry ->
|
zip.entries().asSequence().forEach { entry ->
|
||||||
@@ -261,10 +305,30 @@ object PluginEngine : PluginHostBridge {
|
|||||||
fun ensureBundledPlugins(bundledDir: File) {
|
fun ensureBundledPlugins(bundledDir: File) {
|
||||||
if (!bundledDir.exists()) return
|
if (!bundledDir.exists()) return
|
||||||
bundledDir.listFiles()?.filter { it.extension == "fcplugin" }?.forEach { archive ->
|
bundledDir.listFiles()?.filter { it.extension == "fcplugin" }?.forEach { archive ->
|
||||||
val installed = pluginsRootProvider().resolve("hello_world")
|
val manifestInArchive = readManifestFromArchive(archive) ?: return@forEach
|
||||||
if (!installed.exists()) {
|
val installedDir = pluginsRootProvider().resolve(manifestInArchive.id)
|
||||||
|
val installedManifest = installedDir.resolve("manifest.json")
|
||||||
|
val shouldInstall = when {
|
||||||
|
!installedManifest.exists() -> true
|
||||||
|
else -> {
|
||||||
|
val current = runCatching {
|
||||||
|
PluginManifestParser.parse(installedManifest.readText())
|
||||||
|
}.getOrNull()
|
||||||
|
current == null || current.version != manifestInArchive.version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldInstall) {
|
||||||
runCatching { installFcPluginArchive(archive) }
|
runCatching { installFcPluginArchive(archive) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun readManifestFromArchive(archive: File): PluginManifest? {
|
||||||
|
return runCatching {
|
||||||
|
java.util.zip.ZipFile(archive).use { zip ->
|
||||||
|
val entry = zip.getEntry("manifest.json") ?: return null
|
||||||
|
zip.getInputStream(entry).bufferedReader().use { PluginManifestParser.parse(it.readText()) }
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ object PluginManifestParser {
|
|||||||
description = root["description"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
description = root["description"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||||
author = root["author"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
author = root["author"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||||
version = root["version"]?.jsonPrimitive?.contentOrNull ?: "1.0.0",
|
version = root["version"]?.jsonPrimitive?.contentOrNull ?: "1.0.0",
|
||||||
|
usage = root["usage"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||||
appVersion = root["app_version"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
appVersion = root["app_version"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||||
sdkVersion = root["sdk_version"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
sdkVersion = root["sdk_version"]?.jsonPrimitive?.contentOrNull.orEmpty(),
|
||||||
entryClass = root.requireString("entry_class"),
|
entryClass = root.requireString("entry_class"),
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ tasks.register("packageFcPlugin") {
|
|||||||
{
|
{
|
||||||
"id": "hello_world",
|
"id": "hello_world",
|
||||||
"name": "Hello World",
|
"name": "Hello World",
|
||||||
"description": "Rewrites .hello commands into a friendly greeting",
|
"description": "Demonstrates high-level send hooks and raw Logger.d hooks.",
|
||||||
"author": "FromChat",
|
"author": "FromChat",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
|
"usage": "Send .hello Name in any chat to rewrite the message. Raw hook shows a bulletin when a message is queued for sending.",
|
||||||
"app_version": ">=1.0.0",
|
"app_version": ">=1.0.0",
|
||||||
"sdk_version": ">=1.0.0",
|
"sdk_version": ">=1.0.0",
|
||||||
"entry_class": "ru.fromchat.plugins.sample.hello.HelloWorldPlugin",
|
"entry_class": "ru.fromchat.plugins.sample.hello.HelloWorldPlugin",
|
||||||
|
|||||||
+12
@@ -13,6 +13,17 @@ class HelloWorldPlugin : BasePlugin() {
|
|||||||
addOnSendMessageHook { context ->
|
addOnSendMessageHook { context ->
|
||||||
onSendMessage(context)
|
onSendMessage(context)
|
||||||
}
|
}
|
||||||
|
hookShared(
|
||||||
|
hookId = "logger.debug",
|
||||||
|
after = { args, _ ->
|
||||||
|
val tag = args.getOrNull(0) as? String ?: return@hookShared HookResult()
|
||||||
|
val message = args.getOrNull(1) as? String ?: return@hookShared HookResult()
|
||||||
|
if (tag == "OutgoingMessageCoordinator" && message.contains("enqueue")) {
|
||||||
|
showBulletin("Raw hook: intercepted outgoing message pipeline")
|
||||||
|
}
|
||||||
|
HookResult()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createSettings(): List<PluginSetting> = listOf(
|
override fun createSettings(): List<PluginSetting> = listOf(
|
||||||
@@ -36,6 +47,7 @@ class HelloWorldPlugin : BasePlugin() {
|
|||||||
val name = parts.getOrNull(1)?.trim().orEmpty().ifEmpty { "World" }
|
val name = parts.getOrNull(1)?.trim().orEmpty().ifEmpty { "World" }
|
||||||
val template = getSettingString("template", "Hello, {name}!")
|
val template = getSettingString("template", "Hello, {name}!")
|
||||||
context.text = template.replace("{name}", name)
|
context.text = template.replace("{name}", name)
|
||||||
|
showBulletin("High-level hook: rewrote .hello command")
|
||||||
return HookResult(strategy = HookStrategy.MODIFY, value = context)
|
return HookResult(strategy = HookStrategy.MODIFY, value = context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ data class PluginManifest(
|
|||||||
val description: String = "",
|
val description: String = "",
|
||||||
val author: String = "",
|
val author: String = "",
|
||||||
val version: String = "1.0.0",
|
val version: String = "1.0.0",
|
||||||
|
val usage: String = "",
|
||||||
val appVersion: String = "",
|
val appVersion: String = "",
|
||||||
val sdkVersion: String = "",
|
val sdkVersion: String = "",
|
||||||
val entryClass: String,
|
val entryClass: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user