From ab4f6fcc5335a513b1493e75c848458f1bc46bae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 16 Sep 2026 22:42:35 +0000 Subject: [PATCH] Restyle plugins UI with Material 3 expressive tokens Replace hardcoded exteraGram colors with theme-aware cards, SearchBar, SettingsDetailTopBar, ExpressiveIconFrame, and tonal action buttons. Co-authored-by: denis0001-dev --- .../composeResources/values-ru/strings.xml | 1 + .../composeResources/values/strings.xml | 1 + .../ui/main/settings/PluginsScreen.kt | 332 ++++++++++-------- 3 files changed, 193 insertions(+), 141 deletions(-) diff --git a/app/shared/src/commonMain/composeResources/values-ru/strings.xml b/app/shared/src/commonMain/composeResources/values-ru/strings.xml index dd188ea..6e2bc6f 100644 --- a/app/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/app/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -351,6 +351,7 @@ Включено Настройки плагина Поиск плагинов + Добавить плагин Использование: Версия %1$s • @%2$s Удалить diff --git a/app/shared/src/commonMain/composeResources/values/strings.xml b/app/shared/src/commonMain/composeResources/values/strings.xml index 545f029..7f1b291 100644 --- a/app/shared/src/commonMain/composeResources/values/strings.xml +++ b/app/shared/src/commonMain/composeResources/values/strings.xml @@ -382,6 +382,7 @@ Enabled Plugin settings Search plugins + Add plugin Usage: Version %1$s • @%2$s Delete diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/PluginsScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/PluginsScreen.kt index 2fe68ee..c0997f4 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/PluginsScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/main/settings/PluginsScreen.kt @@ -1,8 +1,8 @@ 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.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -10,31 +10,30 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height 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.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.Delete 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.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialShapes import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField 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.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -44,7 +43,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.pr0gramm3r101.components.Category @@ -52,8 +51,8 @@ import com.pr0gramm3r101.components.ListItem import com.pr0gramm3r101.components.SwitchListItem import com.pr0gramm3r101.utils.supportClipboardManagerImpl import com.pr0gramm3r101.utils.verticalScroll -import org.jetbrains.compose.resources.stringResource import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.stringResource import ru.fromchat.Res import ru.fromchat.back import ru.fromchat.cancel @@ -62,7 +61,10 @@ import ru.fromchat.plugins.PluginSetting import ru.fromchat.plugins.host.PluginEngine import ru.fromchat.plugins.integration.PluginInstallSupport import ru.fromchat.ui.LocalNavController +import ru.fromchat.ui.components.ExpressiveIconFrame +import ru.fromchat.ui.components.SearchBar import ru.fromchat.ui.components.Text +import ru.fromchat.plugins_add import ru.fromchat.plugins_delete import ru.fromchat.plugins_delete_confirm import ru.fromchat.plugins_developer_mode @@ -80,16 +82,13 @@ 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, ExperimentalMaterial3ExpressiveApi::class) @Composable fun PluginsScreen() { val navController = LocalNavController.current val scope = rememberCoroutineScope() val clipboard = supportClipboardManagerImpl + val scrollBehavior = rememberSettingsCollapsingScrollBehavior() val engineEnabled by PluginEngine.engineEnabled.collectAsState() val developerMode by PluginEngine.developerMode.collectAsState() val installed by PluginEngine.installed.collectAsState() @@ -119,8 +118,15 @@ fun PluginsScreen() { title = { Text(stringResource(Res.string.plugins_info_title)) }, text = { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text(stringResource(Res.string.plugins_info_body)) - Text(stringResource(Res.string.plugins_engine_d)) + Text( + stringResource(Res.string.plugins_info_body), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + stringResource(Res.string.plugins_engine_d), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall, + ) SwitchListItem( headline = stringResource(Res.string.plugins_developer_mode), supportingText = stringResource(Res.string.plugins_developer_mode_d), @@ -150,7 +156,10 @@ fun PluginsScreen() { deleteTarget = null }, ) { - Text(stringResource(Res.string.plugins_delete)) + Text( + stringResource(Res.string.plugins_delete), + color = MaterialTheme.colorScheme.error, + ) } }, dismissButton = { @@ -174,9 +183,12 @@ fun PluginsScreen() { } Scaffold( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = MaterialTheme.colorScheme.background, topBar = { - TopAppBar( + SettingsDetailTopBar( title = { Text( stringResource(Res.string.plugins_title), @@ -184,31 +196,10 @@ fun PluginsScreen() { 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, - ), + onBack = { navController.navigateUp() }, + scrollBehavior = scrollBehavior, ) }, - containerColor = Color.Black, ) { innerPadding -> Column( Modifier @@ -217,18 +208,38 @@ fun PluginsScreen() { .padding(innerPadding) .padding(horizontal = 16.dp), ) { - OutlinedTextField( - value = searchQuery, - onValueChange = { searchQuery = it }, + SearchBar( + query = searchQuery, + onQueryChange = { searchQuery = it }, + onSearch = {}, + placeholder = stringResource(Res.string.plugins_search), 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, + .padding(top = 8.dp, bottom = 16.dp), ) - PluginMasterToggle( + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + FilledTonalIconButton(onClick = { PluginInstallSupport.installFromPicker() }) { + Icon( + Icons.Outlined.Add, + contentDescription = stringResource(Res.string.plugins_add), + ) + } + IconButton(onClick = { showInfoDialog = true }) { + Icon( + Icons.Default.Info, + contentDescription = stringResource(Res.string.plugins_info_title), + ) + } + } + + PluginEngineMasterCard( checked = engineEnabled, onCheckedChange = PluginEngine::setEngineEnabled, ) @@ -242,7 +253,8 @@ fun PluginsScreen() { } else { stringResource(Res.string.plugins_no_results) }, - color = Color.White.copy(alpha = 0.7f), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(vertical = 24.dp), ) } else { @@ -277,43 +289,46 @@ fun PluginsScreen() { } @Composable -private fun PluginMasterToggle( +private fun PluginEngineMasterCard( checked: Boolean, onCheckedChange: (Boolean) -> Unit, ) { - Surface( - modifier = Modifier - .fillMaxWidth() - .clickable { onCheckedChange(!checked) }, - shape = RoundedCornerShape(12.dp), - color = PluginMasterBlue, + Card( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.extraLarge, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 14.dp), + .padding(horizontal = 20.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Text( - text = stringResource(Res.string.plugins_enable_system), - color = Color.White, - style = MaterialTheme.typography.titleMedium, - ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(Res.string.plugins_enable_system), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Text( + text = stringResource(Res.string.plugins_engine_d), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f), + modifier = Modifier.padding(top = 4.dp), + ) + } 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), - ), ) } } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun PluginCard( manifest: PluginManifest, @@ -327,10 +342,12 @@ private fun PluginCard( onDelete: () -> Unit, modifier: Modifier = Modifier, ) { - Surface( + Card( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - color = PluginCardBackground, + shape = MaterialTheme.shapes.extraLarge, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), ) { Column(Modifier.padding(16.dp)) { Row( @@ -338,75 +355,66 @@ private fun PluginCard( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - imageVector = Icons.Default.Extension, - contentDescription = null, - tint = PluginMasterBlue, - modifier = Modifier.size(40.dp), + ExpressiveIconFrame( + icon = Icons.Default.Extension, + containerSize = 48.dp, + iconSize = 24.dp, + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + materialPolygon = MaterialShapes.SoftBurst, ) Switch( checked = enabled, onCheckedChange = onEnabledChange, enabled = engineEnabled, - colors = SwitchDefaults.colors( - checkedThumbColor = PluginMasterBlue, - checkedTrackColor = PluginMasterBlue.copy(alpha = 0.45f), - ), ) } - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(12.dp)) Text( text = manifest.name, - color = Color.White, style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, ) val authorLabel = manifest.author.removePrefix("@") - if (authorLabel.isNotBlank()) { - Text( - text = stringResource( + Text( + text = if (authorLabel.isNotBlank()) { + 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), - ) - } + ) + } else { + manifest.version + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + 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, + color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(top = 12.dp), ) } - val usage = manifest.usage.ifBlank { - manifest.description - } - if (usage.isNotBlank()) { + val usage = manifest.usage.takeIf { it.isNotBlank() } + if (usage != null) { Text( text = stringResource(Res.string.plugins_usage), - color = Color.White, style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(top = 12.dp), ) Text( text = usage, - color = Color.White.copy(alpha = 0.75f), style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 4.dp), ) } @@ -418,18 +426,33 @@ private fun PluginCard( 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, - ) + FilledTonalIconButton(onClick = onShare) { + Icon(Icons.Default.Share, contentDescription = null) + } + FilledTonalIconButton(onClick = onOpenSettings) { + Icon(Icons.AutoMirrored.Filled.OpenInNew, contentDescription = null) + } + FilledTonalIconButton(onClick = onTogglePin) { + Icon( + imageVector = if (pinned) Icons.Default.PushPin else Icons.Outlined.PushPin, + contentDescription = null, + tint = if (pinned) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } Spacer(Modifier.weight(1f)) - IconButton(onClick = onDelete) { + IconButton( + onClick = onDelete, + colors = IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { Icon( Icons.Default.Delete, contentDescription = stringResource(Res.string.plugins_delete), - tint = PluginDestructiveRed, ) } } @@ -437,31 +460,21 @@ private fun PluginCard( } } -@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, ExperimentalMaterial3ExpressiveApi::class) @Composable fun PluginDetailScreen(pluginId: String) { val navController = LocalNavController.current + val scrollBehavior = rememberSettingsCollapsingScrollBehavior() val plugin = PluginEngine.loadedPlugin(pluginId) val settings = plugin?.instance?.createSettings().orEmpty() Scaffold( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = MaterialTheme.colorScheme.background, topBar = { - TopAppBar( + SettingsDetailTopBar( title = { Text( plugin?.manifest?.name ?: pluginId, @@ -469,11 +482,8 @@ fun PluginDetailScreen(pluginId: String) { overflow = TextOverflow.Ellipsis, ) }, - navigationIcon = { - IconButton(onClick = { navController.navigateUp() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(Res.string.back)) - } - }, + onBack = { navController.navigateUp() }, + scrollBehavior = scrollBehavior, ) }, ) { innerPadding -> @@ -483,10 +493,49 @@ fun PluginDetailScreen(pluginId: String) { .verticalScroll() .padding(innerPadding), ) { - Category(Modifier.padding(top = 16.dp)) { + if (plugin != null) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ExpressiveIconFrame( + icon = Icons.Default.Extension, + materialPolygon = MaterialShapes.Cookie6Sided, + ) + Spacer(Modifier.height(8.dp)) + Text( + text = plugin.manifest.name, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + ) + val authorLabel = plugin.manifest.author.removePrefix("@") + if (authorLabel.isNotBlank()) { + Text( + text = stringResource( + Res.string.plugins_version_author, + plugin.manifest.version, + authorLabel, + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp), + ) + } + } + } + + Category( + modifier = Modifier.padding(top = 8.dp), + margin = PaddingValues(horizontal = 16.dp, vertical = 0.dp), + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { Text( text = stringResource(Res.string.plugins_settings_title), modifier = Modifier.padding(horizontal = SettingsStepHorizontalPadding, vertical = 8.dp), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, ) settings.forEach { setting -> when (setting) { @@ -494,6 +543,7 @@ fun PluginDetailScreen(pluginId: String) { Text( text = setting.text, modifier = Modifier.padding(horizontal = SettingsStepHorizontalPadding, vertical = 8.dp), + style = MaterialTheme.typography.titleSmall, ) } is PluginSetting.Text -> { @@ -528,7 +578,7 @@ fun PluginDetailScreen(pluginId: String) { ), ) } - androidx.compose.material3.OutlinedTextField( + OutlinedTextField( value = text, onValueChange = { value -> text = value