mirror of
https://github.com/amir1376/ab-download-manager.git
synced 2025-02-20 11:43:24 +08:00
Merge pull request #74 from amir1376/feat/merge-topbar-and-titlebar
add merge top bar with title bar option
This commit is contained in:
commit
8ef83d8e7c
@ -20,8 +20,8 @@ import com.abdownloadmanager.desktop.utils.mvi.supportEffects
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.coerceIn
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.abdownloadmanager.desktop.storage.AppSettingsStorage
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import ir.amirab.downloader.downloaditem.DownloadCredentials
|
||||
import ir.amirab.downloader.downloaditem.DownloadJobStatus
|
||||
@ -245,7 +245,9 @@ class HomeComponent(
|
||||
private val downloadSystem: DownloadSystem by inject()
|
||||
private val queueManager: QueueManager by inject()
|
||||
private val pageStorage: PageStatesStorage by inject()
|
||||
private val appSettings: AppSettingsStorage by inject()
|
||||
val filterState = FilterState()
|
||||
val mergeTopBarWithTitleBar = appSettings.mergeTopBarWithTitleBar
|
||||
|
||||
private val homePageStateToPersist = MutableStateFlow(pageStorage.homePageStorage.value)
|
||||
|
||||
|
@ -4,7 +4,6 @@ import com.abdownloadmanager.desktop.pages.home.sections.DownloadList
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.SearchBox
|
||||
import com.abdownloadmanager.desktop.pages.home.sections.category.*
|
||||
import com.abdownloadmanager.utils.compose.WithContentAlpha
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.WindowTitle
|
||||
import ir.amirab.util.compose.IconSource
|
||||
import com.abdownloadmanager.utils.compose.widget.MyIcon
|
||||
import com.abdownloadmanager.desktop.ui.icon.MyIcons
|
||||
@ -37,7 +36,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.abdownloadmanager.desktop.ui.customwindow.*
|
||||
import com.abdownloadmanager.desktop.utils.externaldraggable.DragData
|
||||
|
||||
|
||||
@ -76,6 +78,31 @@ fun HomePage(component: HomeComponent) {
|
||||
component.confirmDelete(it)
|
||||
})
|
||||
}
|
||||
val mergeTopBar = shouldMergeTopBarWithTitleBar(component)
|
||||
if (mergeTopBar) {
|
||||
WindowTitlePosition(
|
||||
TitlePosition(
|
||||
centered = true,
|
||||
afterStart = true,
|
||||
padding = PaddingValues(end = 32.dp)
|
||||
)
|
||||
)
|
||||
WindowStart {
|
||||
HomeMenuBar(component, Modifier.fillMaxHeight())
|
||||
}
|
||||
WindowEnd {
|
||||
HomeSearch(
|
||||
component = component,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
WindowTitlePosition(
|
||||
TitlePosition(centered = false, afterStart = false)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
@ -107,9 +134,11 @@ fun HomePage(component: HomeComponent) {
|
||||
animateFloatAsState(if (isDragging) 0.2f else 1f).value
|
||||
)
|
||||
) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TopBar(component)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
if (!mergeTopBar) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TopBar(component)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
Spacer(
|
||||
Modifier.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
@ -199,6 +228,18 @@ fun HomePage(component: HomeComponent) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun shouldMergeTopBarWithTitleBar(component: HomeComponent): Boolean {
|
||||
val mergeTopBarWithTitleBarInSettings = component.mergeTopBarWithTitleBar.collectAsState().value
|
||||
if (!mergeTopBarWithTitleBarInSettings) return false
|
||||
val density = LocalDensity.current
|
||||
val widthDp = density.run {
|
||||
LocalWindowInfo.current.containerSize.width.toDp()
|
||||
}
|
||||
return widthDp > 700.dp
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ShowDeletePrompts(
|
||||
deletePromptState: DeletePromptState,
|
||||
@ -370,9 +411,15 @@ private fun Categories(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HomeMenuBar(component: HomeComponent) {
|
||||
private fun HomeMenuBar(
|
||||
component: HomeComponent,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val menu = component.menu
|
||||
MenuBar(menu)
|
||||
MenuBar(
|
||||
modifier,
|
||||
menu
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -421,26 +468,39 @@ private fun TopBar(component: HomeComponent) {
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
HomeMenuBar(component)
|
||||
HomeMenuBar(component, Modifier)
|
||||
Box(Modifier.weight(1f))
|
||||
val searchBoxInteractionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val isFocused by searchBoxInteractionSource.collectIsFocusedAsState()
|
||||
SearchBox(
|
||||
text = component.filterState.textToSearch,
|
||||
onTextChange = {
|
||||
component.filterState.textToSearch = it
|
||||
},
|
||||
interactionSource = searchBoxInteractionSource,
|
||||
modifier = Modifier
|
||||
.width(
|
||||
animateDpAsState(
|
||||
if (isFocused) 220.dp else 180.dp
|
||||
).value
|
||||
)
|
||||
|
||||
HomeSearch(
|
||||
component = component,
|
||||
modifier = Modifier,
|
||||
textPadding = PaddingValues(8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeSearch(
|
||||
component: HomeComponent,
|
||||
modifier: Modifier,
|
||||
textPadding: PaddingValues = PaddingValues(horizontal = 8.dp),
|
||||
) {
|
||||
val searchBoxInteractionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val isFocused by searchBoxInteractionSource.collectIsFocusedAsState()
|
||||
SearchBox(
|
||||
text = component.filterState.textToSearch,
|
||||
onTextChange = {
|
||||
component.filterState.textToSearch = it
|
||||
},
|
||||
textPadding = textPadding,
|
||||
interactionSource = searchBoxInteractionSource,
|
||||
modifier = modifier
|
||||
.width(
|
||||
animateDpAsState(
|
||||
if (isFocused) 220.dp else 180.dp
|
||||
).value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp
|
||||
fun SearchBox(
|
||||
text: String,
|
||||
onTextChange: (String) -> Unit,
|
||||
textPadding: PaddingValues = PaddingValues(horizontal = 8.dp),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
placeholder: String = "Search in the List",
|
||||
modifier: Modifier,
|
||||
@ -36,6 +37,7 @@ fun SearchBox(
|
||||
fontSize = textSize,
|
||||
onTextChange = onTextChange,
|
||||
shape = shape,
|
||||
textPadding = textPadding,
|
||||
interactionSource = interactionSource,
|
||||
start = {
|
||||
WithContentAlpha(
|
||||
|
@ -184,6 +184,21 @@ fun themeConfig(
|
||||
)
|
||||
}
|
||||
|
||||
fun mergeTopBarWithTitleBarConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
|
||||
return BooleanConfigurable(
|
||||
title = "Compact Top Bar",
|
||||
description = "Merge top bar with title bar when the main window has enough width",
|
||||
backedBy = appSettings.mergeTopBarWithTitleBar,
|
||||
describe = {
|
||||
if (it) {
|
||||
"Enabled"
|
||||
} else {
|
||||
"Disabled"
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun autoStartConfig(appSettings: AppSettingsStorage): BooleanConfigurable {
|
||||
return BooleanConfigurable(
|
||||
title = "Start On Boot",
|
||||
@ -263,6 +278,7 @@ class SettingsComponent(
|
||||
themeConfig(themeManager, scope),
|
||||
// uiScaleConfig(appSettings),
|
||||
autoStartConfig(appSettings),
|
||||
mergeTopBarWithTitleBarConfig(appSettings),
|
||||
playSoundNotification(appSettings),
|
||||
)
|
||||
|
||||
|
@ -16,6 +16,7 @@ import java.io.File
|
||||
@Serializable
|
||||
data class AppSettingsModel(
|
||||
val theme: String = "dark",
|
||||
val mergeTopBarWithTitleBar: Boolean = false,
|
||||
val threadCount: Int = 5,
|
||||
val dynamicPartCreation: Boolean = true,
|
||||
val useServerLastModifiedTime: Boolean = false,
|
||||
@ -37,6 +38,7 @@ data class AppSettingsModel(
|
||||
object ConfigLens : Lens<MapConfig, AppSettingsModel> {
|
||||
object Keys {
|
||||
val theme = stringKeyOf("theme")
|
||||
val mergeTopBarWithTitleBar = booleanKeyOf("mergeTopBarWithTitleBar")
|
||||
val threadCount = intKeyOf("threadCount")
|
||||
val dynamicPartCreation = booleanKeyOf("dynamicPartCreation")
|
||||
val useServerLastModifiedTime = booleanKeyOf("useServerLastModifiedTime")
|
||||
@ -56,6 +58,7 @@ data class AppSettingsModel(
|
||||
val default by lazy { AppSettingsModel.default }
|
||||
return AppSettingsModel(
|
||||
theme = source.get(Keys.theme) ?: default.theme,
|
||||
mergeTopBarWithTitleBar = source.get(Keys.mergeTopBarWithTitleBar) ?: default.mergeTopBarWithTitleBar,
|
||||
threadCount = source.get(Keys.threadCount) ?: default.threadCount,
|
||||
dynamicPartCreation = source.get(Keys.dynamicPartCreation) ?: default.dynamicPartCreation,
|
||||
useServerLastModifiedTime = source.get(Keys.useServerLastModifiedTime) ?: default.useServerLastModifiedTime,
|
||||
@ -74,6 +77,7 @@ data class AppSettingsModel(
|
||||
override fun set(source: MapConfig, focus: AppSettingsModel): MapConfig {
|
||||
return source.apply {
|
||||
put(Keys.theme, focus.theme)
|
||||
put(Keys.mergeTopBarWithTitleBar, focus.mergeTopBarWithTitleBar)
|
||||
put(Keys.threadCount, focus.threadCount)
|
||||
put(Keys.dynamicPartCreation, focus.dynamicPartCreation)
|
||||
put(Keys.useServerLastModifiedTime, focus.useServerLastModifiedTime)
|
||||
@ -94,6 +98,7 @@ class AppSettingsStorage(
|
||||
settings: DataStore<MapConfig>,
|
||||
) : ConfigBaseSettings<AppSettingsModel>(settings, AppSettingsModel.ConfigLens) {
|
||||
var theme = from(AppSettingsModel.theme)
|
||||
var mergeTopBarWithTitleBar = from(AppSettingsModel.mergeTopBarWithTitleBar)
|
||||
val threadCount = from(AppSettingsModel.threadCount)
|
||||
val dynamicPartCreation = from(AppSettingsModel.dynamicPartCreation)
|
||||
val useServerLastModifiedTime = from(AppSettingsModel.useServerLastModifiedTime)
|
||||
|
@ -46,10 +46,12 @@ private fun FrameWindowScope.CustomWindowFrame(
|
||||
onRequestClose: () -> Unit,
|
||||
onRequestToggleMaximize: (() -> Unit)?,
|
||||
title: String,
|
||||
titlePosition: TitlePosition,
|
||||
windowIcon: Painter? = null,
|
||||
background: Color,
|
||||
onBackground: Color,
|
||||
center: @Composable () -> Unit,
|
||||
start: (@Composable () -> Unit)?,
|
||||
end: (@Composable () -> Unit)?,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
// val borderColor = MaterialTheme.colors.surface
|
||||
@ -66,7 +68,9 @@ private fun FrameWindowScope.CustomWindowFrame(
|
||||
SnapDraggableToolbar(
|
||||
title = title,
|
||||
windowIcon = windowIcon,
|
||||
center = center,
|
||||
titlePosition = titlePosition,
|
||||
start = start,
|
||||
end = end,
|
||||
onRequestMinimize = onRequestMinimize,
|
||||
onRequestClose = onRequestClose,
|
||||
onRequestToggleMaximize = onRequestToggleMaximize
|
||||
@ -95,14 +99,25 @@ fun isWindowFloating(): Boolean {
|
||||
fun FrameWindowScope.SnapDraggableToolbar(
|
||||
title: String,
|
||||
windowIcon: Painter? = null,
|
||||
center: @Composable () -> Unit,
|
||||
titlePosition: TitlePosition,
|
||||
start: (@Composable () -> Unit)?,
|
||||
end: (@Composable () -> Unit)?,
|
||||
onRequestMinimize: (() -> Unit)?,
|
||||
onRequestToggleMaximize: (() -> Unit)?,
|
||||
onRequestClose: () -> Unit,
|
||||
) {
|
||||
ProvideWindowSpotContainer {
|
||||
if (CustomWindowDecorationAccessing.isSupported) {
|
||||
FrameContent(title, windowIcon, center, onRequestMinimize, onRequestToggleMaximize, onRequestClose)
|
||||
FrameContent(
|
||||
title = title,
|
||||
windowIcon = windowIcon,
|
||||
titlePosition = titlePosition,
|
||||
start = start,
|
||||
end = end,
|
||||
onRequestMinimize = onRequestMinimize,
|
||||
onRequestToggleMaximize = onRequestToggleMaximize,
|
||||
onRequestClose = onRequestClose
|
||||
)
|
||||
} else {
|
||||
WindowDraggableArea(
|
||||
Modifier.onClick(
|
||||
@ -112,7 +127,16 @@ fun FrameWindowScope.SnapDraggableToolbar(
|
||||
onClick = {}
|
||||
)
|
||||
) {
|
||||
FrameContent(title, windowIcon, center, onRequestMinimize, onRequestToggleMaximize, onRequestClose)
|
||||
FrameContent(
|
||||
title = title,
|
||||
windowIcon = windowIcon,
|
||||
titlePosition = titlePosition,
|
||||
start = start,
|
||||
end = end,
|
||||
onRequestMinimize = onRequestMinimize,
|
||||
onRequestToggleMaximize = onRequestToggleMaximize,
|
||||
onRequestClose = onRequestClose
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,7 +146,9 @@ fun FrameWindowScope.SnapDraggableToolbar(
|
||||
private fun FrameWindowScope.FrameContent(
|
||||
title: String,
|
||||
windowIcon: Painter? = null,
|
||||
center: @Composable () -> Unit,
|
||||
titlePosition: TitlePosition,
|
||||
start: (@Composable () -> Unit)?,
|
||||
end: (@Composable () -> Unit)?,
|
||||
onRequestMinimize: (() -> Unit)?,
|
||||
onRequestToggleMaximize: (() -> Unit)?,
|
||||
onRequestClose: () -> Unit,
|
||||
@ -150,20 +176,48 @@ private fun FrameWindowScope.FrameContent(
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
WithContentColor(myColors.onBackground) {
|
||||
WithContentAlpha(1f) {
|
||||
Text(
|
||||
title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = myTextSizes.base,
|
||||
modifier = Modifier
|
||||
.windowFrameItem("title", HitSpots.DRAGGABLE_AREA)
|
||||
)
|
||||
if (!titlePosition.afterStart) {
|
||||
Title(
|
||||
modifier = Modifier
|
||||
.ifThen(titlePosition.centered) {
|
||||
weight(1f)
|
||||
.ifThen(start == null) {
|
||||
wrapContentWidth()
|
||||
}
|
||||
}
|
||||
.padding(titlePosition.padding),
|
||||
title = title
|
||||
)
|
||||
}
|
||||
start?.let {
|
||||
Row(
|
||||
Modifier.windowFrameItem("start", HitSpots.OTHER_HIT_SPOT)
|
||||
) {
|
||||
start()
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
Box(Modifier.weight(1f)) {
|
||||
center()
|
||||
if (titlePosition.afterStart) {
|
||||
Title(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.ifThen(titlePosition.centered) {
|
||||
wrapContentWidth()
|
||||
}
|
||||
.padding(titlePosition.padding),
|
||||
title = title
|
||||
)
|
||||
}
|
||||
if (!titlePosition.centered && !titlePosition.afterStart) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
end?.let {
|
||||
Row(
|
||||
Modifier.windowFrameItem("end", HitSpots.OTHER_HIT_SPOT)
|
||||
) {
|
||||
end()
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowsActionButtons(
|
||||
@ -174,6 +228,25 @@ private fun FrameWindowScope.FrameContent(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FrameWindowScope.Title(
|
||||
modifier: Modifier, title: String,
|
||||
) {
|
||||
WithContentColor(myColors.onBackground) {
|
||||
WithContentAlpha(1f) {
|
||||
Text(
|
||||
title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = myTextSizes.base,
|
||||
modifier = Modifier
|
||||
.windowFrameItem("title", HitSpots.DRAGGABLE_AREA)
|
||||
.then(modifier)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultAppIcon: IconSource
|
||||
@Composable
|
||||
get() {
|
||||
@ -207,8 +280,10 @@ fun CustomWindow(
|
||||
preventMinimize: Boolean = onRequestMinimize == null,
|
||||
content: @Composable FrameWindowScope.() -> Unit,
|
||||
) {
|
||||
val center = windowController.center ?: {}
|
||||
val start = windowController.start
|
||||
val end = windowController.end
|
||||
val title = windowController.title.orEmpty()
|
||||
val titlePosition = windowController.titlePosition
|
||||
val icon = windowController.icon ?: defaultAppIcon.rememberPainter()
|
||||
|
||||
|
||||
@ -258,10 +333,12 @@ fun CustomWindow(
|
||||
onRequestClose = onCloseRequest,
|
||||
onRequestToggleMaximize = onRequestToggleMaximize,
|
||||
title = title,
|
||||
titlePosition = titlePosition,
|
||||
windowIcon = icon,
|
||||
background = background,
|
||||
onBackground = myColors.onBackground,
|
||||
center = { center() }
|
||||
start = start,
|
||||
end = end,
|
||||
) {
|
||||
// val defaultDensity = LocalDensity.current
|
||||
// val uiScale = LocalUiScale.current
|
||||
@ -318,8 +395,21 @@ class WindowController(
|
||||
icon: Painter? = null,
|
||||
) {
|
||||
var title by mutableStateOf(title)
|
||||
var titlePosition by mutableStateOf(TitlePosition.default())
|
||||
var icon by mutableStateOf(icon)
|
||||
var center: (@Composable () -> Unit)? by mutableStateOf(null)
|
||||
var start: (@Composable () -> Unit)? by mutableStateOf(null)
|
||||
var end: (@Composable () -> Unit)? by mutableStateOf(null)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class TitlePosition(
|
||||
val centered: Boolean = false,
|
||||
val afterStart: Boolean = false,
|
||||
val padding: PaddingValues = PaddingValues(0.dp),
|
||||
) {
|
||||
companion object {
|
||||
fun default() = TitlePosition()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -345,12 +435,23 @@ private val LocalWindowController = compositionLocalOf<WindowController> { error
|
||||
private val LocalWindowState = compositionLocalOf<WindowState> { error("window controller not provided") }
|
||||
|
||||
@Composable
|
||||
fun WindowCenter(content: @Composable () -> Unit) {
|
||||
fun WindowStart(content: @Composable () -> Unit) {
|
||||
val c = LocalWindowController.current
|
||||
c.center = content
|
||||
c.start = content
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
c.center = null
|
||||
c.start = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WindowEnd(content: @Composable () -> Unit) {
|
||||
val c = LocalWindowController.current
|
||||
c.end = content
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
c.end = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -368,6 +469,19 @@ fun WindowTitle(title: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WindowTitlePosition(titlePosition: TitlePosition) {
|
||||
val c = LocalWindowController.current
|
||||
LaunchedEffect(titlePosition) {
|
||||
c.titlePosition = titlePosition
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
c.titlePosition = TitlePosition.default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WindowIcon(icon: IconSource) {
|
||||
WindowIcon(icon.rememberPainter())
|
||||
|
@ -50,6 +50,7 @@ val LocalMenuBoxClip = compositionLocalOf<Shape> {
|
||||
|
||||
@Composable
|
||||
fun MenuBar(
|
||||
modifier: Modifier = Modifier,
|
||||
subMenuList: List<MenuItem.SubMenu>,
|
||||
) {
|
||||
var openedItem: MenuItem.SubMenu? by remember {
|
||||
@ -65,7 +66,7 @@ fun MenuBar(
|
||||
val isSelected = openedItem == subMenu
|
||||
Column {
|
||||
Column(
|
||||
Modifier
|
||||
modifier
|
||||
.clickable {
|
||||
openedItem = subMenu
|
||||
}
|
||||
@ -73,6 +74,7 @@ fun MenuBar(
|
||||
background(myColors.surface)
|
||||
}
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.wrapContentHeight(Alignment.CenterVertically)
|
||||
) {
|
||||
val text = subMenu.title.collectAsState().value
|
||||
val (firstChar, leadingText) = remember(text) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user