mirror of
https://github.com/openflocon/Flocon.git
synced 2026-05-06 05:18:41 +00:00
Add collapse / expand to LeftPanel (#54)
* feature: Add DS module * feature: Use FloconTheme * feature: Migrate MaterialTheme to FloconTheme * fix: Merge * feature: Add wide navigation rail * feature: Rework panel * feature: Rework panel * fix: Discussion * fix: Size --------- Co-authored-by: Raphael Teyssandier <rteyssandier@sephora.fr>
This commit is contained in:
parent
af9d975123
commit
9e0e3b09df
10 changed files with 468 additions and 251 deletions
|
|
@ -1,17 +1,38 @@
|
|||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.github.openflocon.flocondesktop.main.ui
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ChevronRight
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.github.openflocon.flocondesktop.common.ui.FloconColors
|
||||
import io.github.openflocon.flocondesktop.features.analytics.ui.view.AnalyticsScreen
|
||||
import io.github.openflocon.flocondesktop.features.dashboard.ui.view.DashboardScreen
|
||||
import io.github.openflocon.flocondesktop.features.database.ui.view.DatabaseScreen
|
||||
|
|
@ -28,6 +49,9 @@ import io.github.openflocon.flocondesktop.main.ui.model.leftpanel.LeftPanelItem
|
|||
import io.github.openflocon.flocondesktop.main.ui.model.leftpanel.LeftPanelState
|
||||
import io.github.openflocon.flocondesktop.main.ui.settings.SettingsScreen
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.leftpannel.LeftPanelView
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.leftpannel.PanelMaxWidth
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.leftpannel.PanelMinWidth
|
||||
import io.github.openflocon.library.designsystem.components.FloconIcon
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
|
||||
@Composable
|
||||
|
|
@ -60,87 +84,122 @@ private fun MainScreen(
|
|||
onClickLeftPanelItem: (LeftPanelItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier) {
|
||||
// TODO navigation
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
LeftPanelView(
|
||||
modifier = Modifier.width(300.dp)
|
||||
.fillMaxHeight(),
|
||||
onClickItem = onClickLeftPanelItem,
|
||||
state = leftPanelState,
|
||||
devicesState = devicesState,
|
||||
onDeviceSelected = onDeviceSelected,
|
||||
)
|
||||
var expanded by remember { mutableStateOf(true) }
|
||||
val width by animateDpAsState(targetValue = if (expanded) PanelMaxWidth else PanelMinWidth)
|
||||
var windowSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
val position by animateDpAsState(
|
||||
targetValue = if (expanded) PanelMaxWidth else PanelMinWidth
|
||||
)
|
||||
val rotate by animateFloatAsState(targetValue = if (expanded) 180f else 0f)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().shadow(6.dp)) {
|
||||
when (subScreen) {
|
||||
SubScreen.Network ->
|
||||
NetworkScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.onGloballyPositioned {
|
||||
windowSize = it.size // TODO Add windowsize lib
|
||||
}
|
||||
) {
|
||||
LeftPanelView(
|
||||
modifier = Modifier
|
||||
.width(width)
|
||||
.fillMaxHeight(),
|
||||
expanded = expanded,
|
||||
onClickItem = onClickLeftPanelItem,
|
||||
state = leftPanelState,
|
||||
devicesState = devicesState,
|
||||
onDeviceSelected = onDeviceSelected
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.shadow(6.dp)
|
||||
) {
|
||||
when (subScreen) {
|
||||
SubScreen.Network ->
|
||||
NetworkScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
SubScreen.Database ->
|
||||
DatabaseScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
SubScreen.Database ->
|
||||
DatabaseScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
SubScreen.Images ->
|
||||
ImagesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
SubScreen.Images ->
|
||||
ImagesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
SubScreen.Files ->
|
||||
FilesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
SubScreen.Files ->
|
||||
FilesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
SubScreen.Tables ->
|
||||
TableScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
SubScreen.Tables ->
|
||||
TableScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
SubScreen.SharedPreferences ->
|
||||
SharedPreferencesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
SubScreen.SharedPreferences ->
|
||||
SharedPreferencesScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
SubScreen.Dashboard ->
|
||||
DashboardScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
SubScreen.Dashboard ->
|
||||
DashboardScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
||||
SubScreen.Settings -> {
|
||||
SettingsScreen(
|
||||
modifier =
|
||||
SubScreen.Settings -> {
|
||||
SettingsScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SubScreen.Deeplinks -> {
|
||||
DeeplinkScreen(
|
||||
modifier =
|
||||
SubScreen.Deeplinks -> {
|
||||
DeeplinkScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SubScreen.Analytics -> {
|
||||
AnalyticsScreen(
|
||||
modifier =
|
||||
SubScreen.Analytics -> {
|
||||
AnalyticsScreen(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.width(20.dp)
|
||||
.height(60.dp)
|
||||
.graphicsLayer {
|
||||
translationX = position.toPx() - size.width / 2
|
||||
translationY = (windowSize.height / 2) - (size.height / 2)
|
||||
}
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(FloconColors.pannel) // TODO Change
|
||||
.clickable(onClick = { expanded = !expanded })
|
||||
) {
|
||||
FloconIcon(
|
||||
imageVector = Icons.Outlined.ChevronRight,
|
||||
tint = Color.LightGray,
|
||||
modifier = Modifier.rotate(rotate)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,34 @@
|
|||
|
||||
package io.github.openflocon.flocondesktop.main.ui.view
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PhoneDisabled
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ExposedDropdownMenuBoxScope
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -27,6 +37,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import flocondesktop.composeapp.generated.resources.Res
|
||||
|
|
@ -37,100 +48,142 @@ import io.github.openflocon.flocondesktop.main.ui.model.DeviceItemUiModel
|
|||
import io.github.openflocon.flocondesktop.main.ui.model.DevicesStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.previewDeviceItemUiModelPreview
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.FloconCircularProgressIndicator
|
||||
import io.github.openflocon.library.designsystem.components.FloconIcon
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
@Composable
|
||||
internal fun DeviceSelectorView(
|
||||
pannelExpanded: Boolean,
|
||||
devicesState: DevicesStateUiModel,
|
||||
onDeviceSelected: (DeviceItemUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
var dropDownExpanded by remember { mutableStateOf(false) }
|
||||
val topRadius by animateDpAsState(
|
||||
targetValue = if (dropDownExpanded) 0.dp else 12.dp
|
||||
)
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(pannelExpanded) {
|
||||
if (!pannelExpanded)
|
||||
dropDownExpanded = false
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.shadow(elevation = 4.dp, shape = shape, clip = true)
|
||||
.background(color = FloconColors.pannel)
|
||||
.clickable {
|
||||
expanded = true
|
||||
},
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = dropDownExpanded,
|
||||
onExpandedChange = {
|
||||
if (pannelExpanded)
|
||||
dropDownExpanded = it
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = 8.dp,
|
||||
vertical = 4.dp,
|
||||
),
|
||||
AnimatedContent(
|
||||
targetState = devicesState,
|
||||
modifier = modifier
|
||||
.shadow(
|
||||
elevation = 4.dp,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = topRadius,
|
||||
topEnd = topRadius,
|
||||
bottomEnd = 12.dp,
|
||||
bottomStart = 12.dp
|
||||
),
|
||||
clip = true
|
||||
)
|
||||
.background(color = FloconColors.pannel)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) { targetState ->
|
||||
when (targetState) {
|
||||
DevicesStateUiModel.Empty -> Empty(expanded = pannelExpanded)
|
||||
DevicesStateUiModel.Loading -> Loading()
|
||||
is DevicesStateUiModel.WithDevices -> Device(
|
||||
state = targetState,
|
||||
pannelExpanded = pannelExpanded
|
||||
)
|
||||
}
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
expanded = dropDownExpanded,
|
||||
onDismissRequest = { dropDownExpanded = false },
|
||||
containerColor = FloconColors.pannel, // TODO Change
|
||||
shadowElevation = 0.dp,
|
||||
shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
|
||||
modifier = Modifier
|
||||
.exposedDropdownSize()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
when (devicesState) {
|
||||
DevicesStateUiModel.Loading -> {
|
||||
// hide
|
||||
}
|
||||
|
||||
DevicesStateUiModel.Empty -> {
|
||||
Text(
|
||||
text = "No Devices Found",
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||
style = FloconTheme.typography.bodyMedium,
|
||||
color = FloconTheme.colorScheme.onSurface,
|
||||
if (devicesState is DevicesStateUiModel.WithDevices) {
|
||||
devicesState.devices.forEach { device ->
|
||||
DeviceView(
|
||||
device = device,
|
||||
pannelExpanded = pannelExpanded,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.clickable(enabled = pannelExpanded) {
|
||||
onDeviceSelected(device)
|
||||
dropDownExpanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
is DevicesStateUiModel.WithDevices -> {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it }
|
||||
) {
|
||||
DeviceView(
|
||||
device = devicesState.selected,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
)
|
||||
DropdownMenu( // TODO Change to ExposedDropdownMenu when width is fixed https://issuetracker.google.com/issues/205589613
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier
|
||||
.exposedDropdownSize(matchTextFieldWidth = true)
|
||||
) {
|
||||
devicesState.devices.forEach { device ->
|
||||
DeviceView(
|
||||
device = device,
|
||||
modifier = Modifier.fillMaxWidth().clickable {
|
||||
onDeviceSelected(device)
|
||||
expanded = false // Close the dropdown after selection
|
||||
},
|
||||
)
|
||||
// DropdownMenuItem(
|
||||
// text = {
|
||||
// DeviceView(
|
||||
// device = device,
|
||||
// )
|
||||
// },
|
||||
// onClick = {
|
||||
// onDeviceSelected(device)
|
||||
// expanded = false // Close the dropdown after selection
|
||||
// },
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = Color.LightGray) // TODO Change
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Empty(
|
||||
expanded: Boolean
|
||||
) {
|
||||
Crossfade(expanded) {
|
||||
if (it) {
|
||||
Text(
|
||||
text = "No Devices Found",
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||
style = FloconTheme.typography.bodyMedium,
|
||||
color = FloconTheme.colorScheme.onSurface,
|
||||
)
|
||||
} else {
|
||||
FloconIcon(
|
||||
imageVector = Icons.Outlined.PhoneDisabled,
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.wrapContentHeight()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Loading() {
|
||||
FloconCircularProgressIndicator()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExposedDropdownMenuBoxScope.Device(
|
||||
pannelExpanded: Boolean,
|
||||
state: DevicesStateUiModel.WithDevices
|
||||
) {
|
||||
DeviceView(
|
||||
device = state.selected,
|
||||
pannelExpanded = pannelExpanded,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceView(
|
||||
device: DeviceItemUiModel,
|
||||
pannelExpanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
|
|
@ -138,22 +191,27 @@ private fun DeviceView(
|
|||
painter = painterResource(Res.drawable.smartphone),
|
||||
contentDescription = null,
|
||||
)
|
||||
Column(modifier = Modifier.padding(start = 4.dp)) {
|
||||
Text(
|
||||
text = device.deviceName, // Device Name
|
||||
color = FloconColorScheme.onSurface,
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Text(
|
||||
text = device.appName,
|
||||
color = FloconColorScheme.onSurface.copy(alpha = 0.5f),
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Thin),
|
||||
)
|
||||
Text(
|
||||
text = device.appPackageName,
|
||||
color = FloconColorScheme.onSurface.copy(alpha = 0.5f),
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Thin),
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = pannelExpanded,
|
||||
exit = fadeOut(tween(100))
|
||||
) {
|
||||
Column(modifier = Modifier.padding(start = 4.dp)) {
|
||||
Text(
|
||||
text = device.deviceName, // Device Name
|
||||
color = FloconColorScheme.onSurface,
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Text(
|
||||
text = device.appName,
|
||||
color = FloconColorScheme.onSurface.copy(alpha = 0.5f),
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Thin),
|
||||
)
|
||||
Text(
|
||||
text = device.appPackageName,
|
||||
color = FloconColorScheme.onSurface.copy(alpha = 0.5f),
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Thin),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,6 +222,7 @@ private fun DeviceViewPreview() {
|
|||
FloconTheme {
|
||||
DeviceView(
|
||||
device = previewDeviceItemUiModelPreview(),
|
||||
pannelExpanded = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ fun LeftPannelDivider(modifier: Modifier = Modifier) {
|
|||
HorizontalDivider(
|
||||
modifier = modifier.padding(horizontal = 4.dp),
|
||||
thickness = 1.dp,
|
||||
color = Color.Gray,
|
||||
color = Color.Gray, // TODO Change
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,25 @@
|
|||
@file:Suppress("UnusedReceiverParameter")
|
||||
|
||||
package io.github.openflocon.flocondesktop.main.ui.view.leftpannel
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -36,16 +41,23 @@ import io.github.openflocon.flocondesktop.main.ui.model.DeviceItemUiModel
|
|||
import io.github.openflocon.flocondesktop.main.ui.model.DevicesStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.leftpanel.LeftPanelItem
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.leftpanel.LeftPanelState
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.leftpanel.LeftPannelSection
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.leftpanel.previewLeftPannelState
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.previewDevicesStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.DeviceSelectorView
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import java.awt.Menu
|
||||
|
||||
val PanelMaxWidth = 275.dp
|
||||
val PanelMinWidth = 64.dp
|
||||
val PanelContentMinSize = 40.dp
|
||||
|
||||
@Composable
|
||||
fun LeftPanelView(
|
||||
state: LeftPanelState,
|
||||
expanded: Boolean,
|
||||
onClickItem: (LeftPanelItem) -> Unit,
|
||||
devicesState: DevicesStateUiModel, // Pass the state of devices
|
||||
onDeviceSelected: (DeviceItemUiModel) -> Unit, // Callback when a device is selected
|
||||
|
|
@ -53,83 +65,105 @@ fun LeftPanelView(
|
|||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(FloconColorScheme.surface)
|
||||
.background(FloconTheme.colorScheme.surface)
|
||||
.padding(horizontal = 12.dp, vertical = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
Title(expanded = expanded)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
MenuSection(
|
||||
items = state.sections,
|
||||
expanded = expanded,
|
||||
onClickItem = onClickItem
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(Modifier.weight(1f))
|
||||
MenuItems(
|
||||
items = state.bottomItems,
|
||||
expanded = expanded,
|
||||
onClickItem = onClickItem
|
||||
)
|
||||
LeftPannelDivider(modifier = Modifier.padding(vertical = 12.dp))
|
||||
DeviceSelectorView(
|
||||
pannelExpanded = expanded,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp),
|
||||
devicesState = devicesState,
|
||||
onDeviceSelected = onDeviceSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Title(
|
||||
expanded: Boolean
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.height(PanelContentMinSize),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(PanelContentMinSize)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
painter = painterResource(Res.drawable.app_icon_small),
|
||||
contentDescription = "Description de mon image",
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = expanded,
|
||||
enter = fadeIn() + slideInHorizontally(),
|
||||
exit = fadeOut() + slideOutHorizontally()
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(40.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
painter = painterResource(Res.drawable.app_icon_small),
|
||||
contentDescription = "Description de mon image",
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
"Flocon",
|
||||
text = "Flocon",
|
||||
fontSize = 32.sp,
|
||||
style = FloconTheme.typography.titleLarge.copy(
|
||||
color = FloconColorScheme.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.height(12.dp),
|
||||
@Composable
|
||||
private fun ColumnScope.MenuSection(
|
||||
items: List<LeftPannelSection>,
|
||||
expanded: Boolean,
|
||||
onClickItem: (LeftPanelItem) -> Unit
|
||||
) {
|
||||
items.fastForEachIndexed { index, section ->
|
||||
PannelLabel(
|
||||
expanded = expanded,
|
||||
text = section.title
|
||||
)
|
||||
MenuItems(
|
||||
items = section.items,
|
||||
expanded = expanded,
|
||||
onClickItem = onClickItem
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
state.sections.fastForEachIndexed { index, section ->
|
||||
PannelLabel(
|
||||
text = section.title,
|
||||
modifier = if (index != 0) Modifier.padding(top = 12.dp) else Modifier,
|
||||
)
|
||||
section.items.fastForEach { item ->
|
||||
PannelView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
icon = item.icon,
|
||||
text = item.text,
|
||||
isSelected = item.isSelected,
|
||||
onClick = {
|
||||
onClickItem(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// settings
|
||||
Column(modifier = Modifier) {
|
||||
state.bottomItems.fastForEach { item ->
|
||||
PannelView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
icon = item.icon,
|
||||
text = item.text,
|
||||
isSelected = item.isSelected,
|
||||
onClick = {
|
||||
onClickItem(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
LeftPannelDivider(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(vertical = 12.dp),
|
||||
)
|
||||
|
||||
DeviceSelectorView(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
devicesState = devicesState,
|
||||
onDeviceSelected = onDeviceSelected,
|
||||
)
|
||||
}
|
||||
@Composable
|
||||
private fun ColumnScope.MenuItems(
|
||||
items: List<LeftPanelItem>,
|
||||
expanded: Boolean,
|
||||
onClickItem: (LeftPanelItem) -> Unit
|
||||
) {
|
||||
items.fastForEach { item ->
|
||||
PannelView(
|
||||
modifier = Modifier
|
||||
.height(PanelContentMinSize)
|
||||
.fillMaxWidth(),
|
||||
icon = item.icon,
|
||||
text = item.text,
|
||||
expanded = expanded,
|
||||
isSelected = item.isSelected,
|
||||
onClick = { onClickItem(item) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +182,8 @@ private fun LeftPannelViewPreview() {
|
|||
},
|
||||
modifier = Modifier.wrapContentHeight(),
|
||||
onDeviceSelected = {},
|
||||
devicesState = previewDevicesStateUiModel(),
|
||||
expanded = false,
|
||||
devicesState = previewDevicesStateUiModel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,15 @@
|
|||
package io.github.openflocon.flocondesktop.main.ui.view.leftpannel
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -12,18 +19,34 @@ import io.github.openflocon.library.designsystem.FloconTheme
|
|||
@Composable
|
||||
fun PannelLabel(
|
||||
text: String,
|
||||
expanded: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
Crossfade(
|
||||
targetState = expanded,
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
bottom = 4.dp,
|
||||
),
|
||||
text = text,
|
||||
style = FloconTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Thin,
|
||||
color = FloconColorScheme.onSurface.copy(alpha = 0.5f),
|
||||
),
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.height(28.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
if (it) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 12.dp, bottom = 4.dp),
|
||||
text = text,
|
||||
style = FloconTheme.typography.bodyLarge.copy(
|
||||
fontWeight = FontWeight.Thin,
|
||||
color = FloconColorScheme.onSurface.copy(alpha = 0.5f),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,28 @@
|
|||
@file:OptIn(ExperimentalSharedTransitionApi::class)
|
||||
|
||||
package io.github.openflocon.flocondesktop.main.ui.view.leftpannel
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
|
|
@ -28,35 +38,49 @@ fun PannelView(
|
|||
icon: ImageVector,
|
||||
text: String,
|
||||
isSelected: Boolean,
|
||||
expanded: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
val shadow by animateDpAsState(
|
||||
targetValue = if (isSelected) 6.dp else 0.dp,
|
||||
label = "shadow"
|
||||
)
|
||||
val color by animateColorAsState(
|
||||
targetValue = if (isSelected) FloconColors.pannel else FloconTheme.colorScheme.surface,
|
||||
label = "color"
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.then(
|
||||
if (isSelected)
|
||||
Modifier
|
||||
.shadow(elevation = 6.dp, shape = shape, clip = true)
|
||||
.background(FloconColors.pannel)
|
||||
else Modifier,
|
||||
)
|
||||
.height(28.dp)
|
||||
.shadow(elevation = shadow, shape = shape, clip = true, ambientColor = color, spotColor = color)
|
||||
.background(color)
|
||||
.clickable(onClick = onClick, interactionSource = null, indication = null)
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
modifier = Modifier
|
||||
.size(16.dp),
|
||||
imageVector = icon,
|
||||
contentDescription = "Description de mon image",
|
||||
tint = FloconColorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = text,
|
||||
color = FloconColorScheme.onSurface,
|
||||
style = FloconTheme.typography.bodyMedium,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
expanded,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(tween(100))
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = FloconColorScheme.onSurface,
|
||||
style = FloconTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +93,7 @@ private fun PannelViewPreview() {
|
|||
text = "text",
|
||||
isSelected = false,
|
||||
onClick = {},
|
||||
expanded = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +107,7 @@ private fun PannelViewPreview_Selected() {
|
|||
text = "text",
|
||||
isSelected = true,
|
||||
onClick = {},
|
||||
expanded = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ room = "2.7.1"
|
|||
ksp = "2.2.0-2.0.2"
|
||||
ktlintGradle = "13.0.0"
|
||||
aboutLibraries = "12.2.4"
|
||||
other-molecule = "2.1.0"
|
||||
kotlinStdlib = "2.2.0"
|
||||
runner = "1.5.2"
|
||||
core = "1.5.0"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package io.github.openflocon.library.designsystem.components
|
||||
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun FloconCircularProgressIndicator(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
CircularProgressIndicator(modifier = modifier)
|
||||
}
|
||||
|
|
@ -1,18 +1,22 @@
|
|||
package io.github.openflocon.library.designsystem.components
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
@Composable
|
||||
fun FloconIcon(
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
tint: Color = LocalContentColor.current
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
tint = tint,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ internal val FloconDarkColorScheme = darkColorScheme(
|
|||
onPrimaryContainer = FloconColors.onPrimary,
|
||||
secondary = FloconColors.secondary,
|
||||
onSecondary = FloconColors.onSecondary,
|
||||
secondaryContainer = FloconColors.secondary.copy(alpha = 0.2f),
|
||||
secondaryContainer = FloconColors.secondary,//.copy(alpha = 0.2f),
|
||||
onSecondaryContainer = FloconColors.onSecondary,
|
||||
tertiary = FloconColors.tertiary,
|
||||
onTertiary = FloconColors.onTertiary,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue