Merge pull request #74 from amir1376/feat/merge-topbar-and-titlebar

add merge top bar with title bar option
This commit is contained in:
AmirHossein Abdolmotallebi 2024-09-14 19:29:50 +03:30 committed by GitHub
commit 8ef83d8e7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 250 additions and 49 deletions

View File

@ -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)

View File

@ -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
)
)
}

View File

@ -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(

View File

@ -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),
)

View File

@ -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)

View File

@ -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())

View File

@ -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) {