From 1d1b974ae95a5a8b45d757fd21ab0e687392169e Mon Sep 17 00:00:00 2001 From: "a.emogurov" Date: Tue, 7 Apr 2026 14:10:57 +0400 Subject: [PATCH 1/3] feature: core screens redesign --- panel-core/build.gradle.kts | 2 + .../core/inapp/compose/DebugPanelScreen.kt | 195 +++++++++++++----- .../core/ui/settings/DebugSettingsActivity.kt | 3 +- .../core/ui/settings/DebugSettingsNavHost.kt | 8 +- .../core/ui/settings/DebugSettingsScreen.kt | 23 ++- 5 files changed, 162 insertions(+), 69 deletions(-) diff --git a/panel-core/build.gradle.kts b/panel-core/build.gradle.kts index 601eba3f..c9ac12e8 100644 --- a/panel-core/build.gradle.kts +++ b/panel-core/build.gradle.kts @@ -58,5 +58,7 @@ dependencies { implementation(stack.timber) implementation(stack.material) + implementation(stack.kotlinx.collections.immutable) + api(androidx.lifecycle.viewmodel) } diff --git a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/inapp/compose/DebugPanelScreen.kt b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/inapp/compose/DebugPanelScreen.kt index 010b8128..a1280fe9 100644 --- a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/inapp/compose/DebugPanelScreen.kt +++ b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/inapp/compose/DebugPanelScreen.kt @@ -3,102 +3,183 @@ package com.redmadrobot.debug.core.inapp.compose import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.ScrollableTabRow -import androidx.compose.material.Tab -import androidx.compose.material.TabRowDefaults -import androidx.compose.material.TabRowDefaults.tabIndicatorOffset -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRowDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.redmadrobot.debug.core.R import com.redmadrobot.debug.core.extension.getAllPlugins -import com.redmadrobot.debug.core.plugin.Plugin +import com.redmadrobot.debug.uikit.components.ThemeSwitcher +import com.redmadrobot.debug.uikit.theme.DebugPanelDimensions +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme import com.redmadrobot.debug.uikit.theme.model.ThemeMode +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch -@Suppress("UnusedParameter") @Composable public fun DebugPanelScreen( themeMode: ThemeMode, + onThemeModeChange: (ThemeMode) -> Unit, onClose: () -> Unit, - onThemeModeChange: (ThemeMode) -> Unit = {} ) { val plugins = remember { getAllPlugins() } - val pluginsName = remember { plugins.map { it.getName() } } + val pluginNames = remember { plugins.map { it.getName() } } val pagerState = rememberPagerState(initialPage = 0, pageCount = { plugins.size }) + val scope = rememberCoroutineScope() - Scaffold( - topBar = { - TopAppBar( - title = { Text(text = stringResource(R.string.debug_panel)) }, - navigationIcon = { - IconButton(onClick = onClose) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_back), - contentDescription = null, - ) - } - }, - backgroundColor = MaterialTheme.colors.background, - elevation = 0.dp, - ) - }, - ) { paddingValues -> - Column(modifier = Modifier.padding(paddingValues)) { - PluginsTabLayout(pluginsName = pluginsName, pagerState = pagerState) - PluginsPager(plugins = plugins, pagerState = pagerState) + Column( + modifier = Modifier + .fillMaxSize() + .background(color = DebugPanelTheme.colors.background.primary), + ) { + PanelTopBar( + themeMode = themeMode, + onThemeModeChange = onThemeModeChange, + onClose = onClose, + ) + PanelTabRow( + pluginNames = pluginNames.toImmutableList(), + selectedIndex = pagerState.currentPage, + onTabClick = { index -> scope.launch { pagerState.animateScrollToPage(index) } }, + ) + HorizontalPager(state = pagerState, modifier = Modifier.weight(weight = 1f)) { page -> + plugins[page].content() } } } @Composable -private fun PluginsTabLayout(pluginsName: List, pagerState: PagerState) { - val scope = rememberCoroutineScope() +private fun PanelTopBar( + themeMode: ThemeMode, + onThemeModeChange: (ThemeMode) -> Unit, + onClose: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .heightIn(min = DebugPanelDimensions.topBarHeight) + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.debug_panel), + style = DebugPanelTheme.typography.titleLarge, + color = DebugPanelTheme.colors.content.primary, + modifier = Modifier + .weight(weight = 1f) + .padding(start = 4.dp), + ) + ThemeSwitcher( + currentMode = themeMode, + onModeSelect = onThemeModeChange, + ) + IconButton(onClick = onClose) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back), + contentDescription = null, + tint = DebugPanelTheme.colors.content.secondary, + modifier = Modifier.size(size = DebugPanelDimensions.iconSizeMedium), + ) + } + } +} - ScrollableTabRow( - selectedTabIndex = pagerState.currentPage, - backgroundColor = MaterialTheme.colors.background, - edgePadding = 16.dp, - indicator = { tabPositions -> - TabRowDefaults.Indicator( +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PanelTabRow( + pluginNames: ImmutableList, + selectedIndex: Int, + onTabClick: (Int) -> Unit, + modifier: Modifier = Modifier +) { + SecondaryScrollableTabRow( + selectedTabIndex = selectedIndex, + modifier = modifier + .fillMaxWidth() + .heightIn(min = DebugPanelDimensions.tabRowHeight) + .background(color = DebugPanelTheme.colors.surface.secondary), + edgePadding = 4.dp, + containerColor = DebugPanelTheme.colors.surface.secondary, + contentColor = DebugPanelTheme.colors.content.secondary, + indicator = { + TabRowDefaults.SecondaryIndicator( modifier = Modifier.tabIndicatorOffset( - currentTabPosition = tabPositions[pagerState.currentPage] + selectedTabIndex = selectedIndex, + matchContentSize = false, ), height = 2.dp, - color = MaterialTheme.colors.secondary + color = DebugPanelTheme.colors.content.accent, ) - } + }, + divider = {}, ) { - pluginsName.forEachIndexed { index, _ -> - Tab( - text = { Text(text = pluginsName[index]) }, - selected = pagerState.currentPage == index, - onClick = { - scope.launch { pagerState.animateScrollToPage(index) } - } - ) + pluginNames.forEachIndexed { index, title -> + key(title) { + PluginTab( + isSelected = selectedIndex == index, + title = title, + onTabClick = { onTabClick(index) } + ) + } } } } @Composable -private fun PluginsPager(plugins: List, pagerState: PagerState) { - HorizontalPager(state = pagerState) { page -> - plugins[page].content() +private fun PluginTab( + title: String, + isSelected: Boolean, + onTabClick: () -> Unit, + modifier: Modifier = Modifier +) { + Tab( + modifier = modifier.padding(horizontal = 4.dp), + selected = isSelected, + onClick = { onTabClick.invoke() }, + text = { PluginTabTitle(title = title, isSelected = isSelected) }, + ) +} + +@Composable +private fun PluginTabTitle( + title: String, + isSelected: Boolean, + modifier: Modifier = Modifier +) { + val titleColor = if (isSelected) { + DebugPanelTheme.colors.content.accent + } else { + DebugPanelTheme.colors.content.secondary } + + Text( + modifier = modifier, + text = title, + style = DebugPanelTheme.typography.labelLarge, + color = titleColor, + ) } diff --git a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsActivity.kt b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsActivity.kt index fc3c8c4f..8f041f7c 100644 --- a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsActivity.kt +++ b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsActivity.kt @@ -11,6 +11,7 @@ import com.redmadrobot.debug.core.DebugPanel import com.redmadrobot.debug.core.extension.getAllPlugins import com.redmadrobot.debug.core.internal.EditablePlugin import com.redmadrobot.debug.uikit.theme.DebugPanelTheme +import kotlinx.collections.immutable.toImmutableList internal class DebugSettingsActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -26,7 +27,7 @@ internal class DebugSettingsActivity : AppCompatActivity() { val pluginItems = remember { getSettingItems() } DebugSettingsNavHost( navController = navController, - pluginItems = pluginItems, + pluginItems = pluginItems.toImmutableList(), ) } } diff --git a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsNavHost.kt b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsNavHost.kt index 16f693fd..17698cc9 100644 --- a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsNavHost.kt +++ b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsNavHost.kt @@ -6,17 +6,21 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.redmadrobot.debug.core.extension.getPlugin import com.redmadrobot.debug.core.internal.EditablePlugin +import kotlinx.collections.immutable.ImmutableList private const val MAIN_SCREEN_ROUTE = "main" @Composable internal fun DebugSettingsNavHost( navController: NavHostController, - pluginItems: List, + pluginItems: ImmutableList, ) { NavHost(navController = navController, startDestination = MAIN_SCREEN_ROUTE) { composable(MAIN_SCREEN_ROUTE) { - DebugSettingsScreen(pluginItems = pluginItems, navController = navController) + DebugSettingsScreen( + pluginItems = pluginItems, + navController = navController + ) } pluginItems.forEach { plugin -> diff --git a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsScreen.kt b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsScreen.kt index 62526445..fcc37861 100644 --- a/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsScreen.kt +++ b/panel-core/src/main/kotlin/com/redmadrobot/debug/core/ui/settings/DebugSettingsScreen.kt @@ -1,26 +1,30 @@ package com.redmadrobot.debug.core.ui.settings +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme +import kotlinx.collections.immutable.ImmutableList @Composable internal fun DebugSettingsScreen( navController: NavController, - pluginItems: List, + pluginItems: ImmutableList, ) { LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .background(color = DebugPanelTheme.colors.background.primary), ) { - items(pluginItems) { item -> + items(items = pluginItems, key = { it.pluginName }) { item -> PluginItem( pluginName = item.pluginName, onClick = { navController.navigate(item.pluginClassName) }, @@ -30,13 +34,14 @@ internal fun DebugSettingsScreen( } @Composable -private fun PluginItem(pluginName: String, onClick: () -> Unit) { +private fun PluginItem(pluginName: String, onClick: () -> Unit, modifier: Modifier = Modifier) { Text( - modifier = Modifier + modifier = modifier .fillMaxSize() .clickable { onClick.invoke() } - .padding(horizontal = 16.dp, vertical = 16.dp), + .padding(all = 16.dp), text = pluginName, - fontSize = 18.sp, + style = DebugPanelTheme.typography.titleMedium, + color = DebugPanelTheme.colors.content.primary, ) } From d1d1b55d7ee46690c158b94061fc3815d8225b93 Mon Sep 17 00:00:00 2001 From: "a.emogurov" Date: Tue, 7 Apr 2026 15:40:59 +0400 Subject: [PATCH 2/3] feature: about app screen redesign --- .../plugin/aboutapp/ui/AboutAppScreen.kt | 152 ++++++++++-------- .../plugin/aboutapp/ui/AboutAppViewModel.kt | 20 +++ .../src/main/res/values/strings.xml | 4 +- 3 files changed, 106 insertions(+), 70 deletions(-) diff --git a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppScreen.kt b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppScreen.kt index 3d0c811e..427fd464 100644 --- a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppScreen.kt +++ b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppScreen.kt @@ -1,31 +1,42 @@ package com.redmadrobot.debug.plugin.aboutapp.ui +import android.content.ClipData +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.defaultMinSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Card -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.Clipboard +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.redmadrobot.debug.core.extension.getPlugin import com.redmadrobot.debug.core.extension.provideViewModel import com.redmadrobot.debug.plugin.aboutapp.AboutAppPlugin import com.redmadrobot.debug.plugin.aboutapp.AboutAppPluginContainer +import com.redmadrobot.debug.plugin.aboutapp.R import com.redmadrobot.debug.plugin.aboutapp.model.AboutAppInfo +import com.redmadrobot.debug.uikit.theme.DebugPanelShapes +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme @Composable internal fun AboutAppScreen( @@ -33,82 +44,85 @@ internal fun AboutAppScreen( getPlugin() .getContainer() .createAboutAppViewModel() - } + }, ) { - val state by viewModel.state.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } - AboutAppLayout(state = state) -} + val state by viewModel.state.collectAsState() + val clipboard = LocalClipboard.current -@Composable -private fun AboutAppLayout(state: AboutAppViewState) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(space = 8.dp), - contentPadding = PaddingValues(all = 16.dp), - ) { - items(items = state.appInfoList, key = { it.id }) { item -> - AboutAppItem(item = item) + LaunchedEffect(Unit) { + viewModel.events.collect { event -> + copyToClipboard(clipboard = clipboard, item = event.item) + val message = event.message.format(event.item.title) + snackbarHostState.showSnackbar(message = message) } } -} -@Composable -private fun AboutAppItem(item: AboutAppInfo, modifier: Modifier = Modifier) { - Card( - modifier = modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 56.dp), - elevation = 4.dp - ) { - Column( + Scaffold( + containerColor = DebugPanelTheme.colors.background.primary, + snackbarHost = { + SnackbarHost( + modifier = Modifier.systemBarsPadding(), + hostState = snackbarHostState + ) + }, + ) { paddingValues -> + LazyColumn( modifier = Modifier - .fillMaxWidth() - .padding(all = 16.dp), - verticalArrangement = Arrangement.spacedBy(space = 4.dp), + .fillMaxSize() + .padding(paddingValues = paddingValues), + contentPadding = PaddingValues(vertical = 8.dp), ) { - Text( - text = item.title, - style = MaterialTheme.typography.caption, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colors.primary, - ) - Text( - text = item.value, - style = MaterialTheme.typography.body1, - color = MaterialTheme.colors.onSurface, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) + items(items = state.appInfoList, key = { it.id }) { item -> + InfoRow( + label = item.title, + value = item.value, + onClick = { message -> + viewModel.onInfoItemClicked(message = message, item = item) + }, + ) + } } } } -@Preview(showBackground = true) @Composable -private fun Preview() { - val previewTitle = "Версия" - val previewValue = "3,14" - val state = remember { - AboutAppViewState( - appInfoList = listOf( - AboutAppInfo(title = previewTitle, value = previewValue), - AboutAppInfo( - title = "Номер билда", - value = "fgkdfjgkdfgjdfkgjdfkjgkdfjgkdfjgkdfjgkdjgskdjgkdgfjdsfgjdsfgdsfgjdsfgdskjfgdsjkfgdjfgdsfg" - ), - AboutAppInfo(title = previewTitle, value = previewValue), - AboutAppInfo(title = previewTitle, value = previewValue), - AboutAppInfo(title = previewTitle, value = previewValue), - AboutAppInfo(title = previewTitle, value = previewValue), - AboutAppInfo(title = previewTitle, value = previewValue), - AboutAppInfo(title = previewTitle, value = previewValue), - AboutAppInfo(title = previewTitle, value = previewValue), - ) +private fun InfoRow( + label: String, + value: String, + onClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + val copiedMessage = stringResource(id = R.string.about_app_copied) + + Row( + modifier = modifier + .fillMaxWidth() + .clip(shape = DebugPanelShapes.medium) + .clickable(onClick = { onClick.invoke(copiedMessage) }) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = label.uppercase(), + style = DebugPanelTheme.typography.labelMedium, + color = DebugPanelTheme.colors.content.tertiary, + modifier = Modifier.padding(end = 12.dp), + ) + Text( + text = value, + style = DebugPanelTheme.typography.bodySmall, + color = DebugPanelTheme.colors.content.primary, + modifier = Modifier.weight(weight = 1f), + overflow = TextOverflow.Ellipsis, ) } +} - MaterialTheme { - AboutAppLayout(state = state) - } +private suspend fun copyToClipboard(clipboard: Clipboard, item: AboutAppInfo) { + val clipData = ClipData.newPlainText(item.title, item.value) + + clipboard.setClipEntry(clipEntry = ClipEntry(clipData)) } diff --git a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppViewModel.kt b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppViewModel.kt index d8b3aa71..bf8ed610 100644 --- a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppViewModel.kt +++ b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppViewModel.kt @@ -1,12 +1,32 @@ package com.redmadrobot.debug.plugin.aboutapp.ui +import androidx.lifecycle.viewModelScope +import com.redmadrobot.debug.core.DebugEvent import com.redmadrobot.debug.core.internal.PluginViewModel import com.redmadrobot.debug.plugin.aboutapp.model.AboutAppInfo +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch internal class AboutAppViewModel(appInfoList: List) : PluginViewModel() { private val _state = MutableStateFlow(value = AboutAppViewState(appInfoList = appInfoList)) val state: StateFlow = _state.asStateFlow() + + private val _events = MutableSharedFlow() + val events = _events.asSharedFlow().distinctUntilChanged() + + fun onInfoItemClicked(message: String, item: AboutAppInfo) { + viewModelScope.launch { + _events.emit(AppInfoSelectionEvent(message = message, item = item)) + } + } } + +internal data class AppInfoSelectionEvent( + val message: String, + val item: AboutAppInfo +) : DebugEvent diff --git a/plugins/plugin-about-app/src/main/res/values/strings.xml b/plugins/plugin-about-app/src/main/res/values/strings.xml index 545704f2..83199879 100644 --- a/plugins/plugin-about-app/src/main/res/values/strings.xml +++ b/plugins/plugin-about-app/src/main/res/values/strings.xml @@ -1,2 +1,4 @@ - + + «%s» is copied + From dddf2b7723935a49f14f92f143f97fd253aef8e2 Mon Sep 17 00:00:00 2001 From: "a.emogurov" Date: Wed, 8 Apr 2026 15:42:47 +0400 Subject: [PATCH 3/3] feature: add ClipboardProvider to about app --- .../debug/plugin/aboutapp/AboutAppPlugin.kt | 2 +- .../plugin/aboutapp/AboutAppPluginContainer.kt | 9 +++++++-- .../debug/plugin/aboutapp/ui/AboutAppScreen.kt | 14 -------------- .../plugin/aboutapp/ui/AboutAppViewModel.kt | 7 ++++++- .../plugin/aboutapp/utils/ClipboardProvider.kt | 17 +++++++++++++++++ 5 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/utils/ClipboardProvider.kt diff --git a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/AboutAppPlugin.kt b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/AboutAppPlugin.kt index 05f42d03..3b5b0e9d 100644 --- a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/AboutAppPlugin.kt +++ b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/AboutAppPlugin.kt @@ -16,7 +16,7 @@ public class AboutAppPlugin( } override fun getPluginContainer(commonContainer: CommonContainer): PluginDependencyContainer { - return AboutAppPluginContainer(appInfoList = appInfoList) + return AboutAppPluginContainer(appInfoList = appInfoList, commonContainer = commonContainer) } override fun getName(): String = NAME diff --git a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/AboutAppPluginContainer.kt b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/AboutAppPluginContainer.kt index 5c726e1d..0fc72148 100644 --- a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/AboutAppPluginContainer.kt +++ b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/AboutAppPluginContainer.kt @@ -1,13 +1,18 @@ package com.redmadrobot.debug.plugin.aboutapp +import com.redmadrobot.debug.core.internal.CommonContainer import com.redmadrobot.debug.core.internal.PluginDependencyContainer import com.redmadrobot.debug.plugin.aboutapp.model.AboutAppInfo import com.redmadrobot.debug.plugin.aboutapp.ui.AboutAppViewModel +import com.redmadrobot.debug.plugin.aboutapp.utils.ClipboardProvider internal class AboutAppPluginContainer( - private val appInfoList: List + private val appInfoList: List, + private val commonContainer: CommonContainer ) : PluginDependencyContainer { fun createAboutAppViewModel(): AboutAppViewModel { - return AboutAppViewModel(appInfoList = appInfoList) + val clipboardProvider = ClipboardProvider(context = commonContainer.context) + + return AboutAppViewModel(appInfoList = appInfoList, clipboardProvider = clipboardProvider) } } diff --git a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppScreen.kt b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppScreen.kt index 427fd464..4bfea8d4 100644 --- a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppScreen.kt +++ b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppScreen.kt @@ -1,6 +1,5 @@ package com.redmadrobot.debug.plugin.aboutapp.ui -import android.content.ClipData import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -23,9 +22,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.ClipEntry -import androidx.compose.ui.platform.Clipboard -import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -34,7 +30,6 @@ import com.redmadrobot.debug.core.extension.provideViewModel import com.redmadrobot.debug.plugin.aboutapp.AboutAppPlugin import com.redmadrobot.debug.plugin.aboutapp.AboutAppPluginContainer import com.redmadrobot.debug.plugin.aboutapp.R -import com.redmadrobot.debug.plugin.aboutapp.model.AboutAppInfo import com.redmadrobot.debug.uikit.theme.DebugPanelShapes import com.redmadrobot.debug.uikit.theme.DebugPanelTheme @@ -47,13 +42,10 @@ internal fun AboutAppScreen( }, ) { val snackbarHostState = remember { SnackbarHostState() } - val state by viewModel.state.collectAsState() - val clipboard = LocalClipboard.current LaunchedEffect(Unit) { viewModel.events.collect { event -> - copyToClipboard(clipboard = clipboard, item = event.item) val message = event.message.format(event.item.title) snackbarHostState.showSnackbar(message = message) } @@ -120,9 +112,3 @@ private fun InfoRow( ) } } - -private suspend fun copyToClipboard(clipboard: Clipboard, item: AboutAppInfo) { - val clipData = ClipData.newPlainText(item.title, item.value) - - clipboard.setClipEntry(clipEntry = ClipEntry(clipData)) -} diff --git a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppViewModel.kt b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppViewModel.kt index bf8ed610..91809ef8 100644 --- a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppViewModel.kt +++ b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/ui/AboutAppViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope import com.redmadrobot.debug.core.DebugEvent import com.redmadrobot.debug.core.internal.PluginViewModel import com.redmadrobot.debug.plugin.aboutapp.model.AboutAppInfo +import com.redmadrobot.debug.plugin.aboutapp.utils.ClipboardProvider import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -12,7 +13,10 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -internal class AboutAppViewModel(appInfoList: List) : PluginViewModel() { +internal class AboutAppViewModel( + appInfoList: List, + private val clipboardProvider: ClipboardProvider, +) : PluginViewModel() { private val _state = MutableStateFlow(value = AboutAppViewState(appInfoList = appInfoList)) val state: StateFlow = _state.asStateFlow() @@ -20,6 +24,7 @@ internal class AboutAppViewModel(appInfoList: List) : PluginViewMo val events = _events.asSharedFlow().distinctUntilChanged() fun onInfoItemClicked(message: String, item: AboutAppInfo) { + clipboardProvider.copyToClipboard(label = item.title, text = item.value) viewModelScope.launch { _events.emit(AppInfoSelectionEvent(message = message, item = item)) } diff --git a/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/utils/ClipboardProvider.kt b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/utils/ClipboardProvider.kt new file mode 100644 index 00000000..7724a532 --- /dev/null +++ b/plugins/plugin-about-app/src/main/kotlin/com/redmadrobot/debug/plugin/aboutapp/utils/ClipboardProvider.kt @@ -0,0 +1,17 @@ +package com.redmadrobot.debug.plugin.aboutapp.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context + +internal class ClipboardProvider(private val context: Context) { + private val clipboardManager: ClipboardManager by lazy { + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + } + + fun copyToClipboard(label: String, text: String) { + val clipData = ClipData.newPlainText(label, text) + + clipboardManager.setPrimaryClip(clipData) + } +}