mirror of
https://github.com/openflocon/Flocon.git
synced 2026-05-19 19:59:39 +00:00
refact: [DESIGN] moved togo in top bar (#156)
Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
parent
1520fd52cd
commit
2876bf1ce4
11 changed files with 541 additions and 491 deletions
|
|
@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||
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.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -52,8 +51,7 @@ import io.github.openflocon.flocondesktop.main.ui.model.SubScreen
|
|||
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.DeviceSelectorView
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.MainScreenTopBar
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.topbar.MainScreenTopBar
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,396 +0,0 @@
|
|||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
@file:Suppress("UnusedReceiverParameter")
|
||||
|
||||
package io.github.openflocon.flocondesktop.main.ui.view
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
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.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material.icons.outlined.KeyboardArrowDown
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuBoxScope
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.Text
|
||||
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.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import flocondesktop.composeapp.generated.resources.Res
|
||||
import flocondesktop.composeapp.generated.resources.smartphone
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.AppsStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceAppUiModel
|
||||
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.previewDeviceItemUiModel
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.FloconCircularProgressIndicator
|
||||
import io.github.openflocon.library.designsystem.components.FloconIcon
|
||||
import io.github.openflocon.library.designsystem.theme.FloconColorPalette
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.jetbrains.skia.Image
|
||||
import kotlin.io.encoding.Base64
|
||||
|
||||
@Composable
|
||||
internal fun DeviceSelectorView(
|
||||
devicesState: DevicesStateUiModel,
|
||||
appsState: AppsStateUiModel,
|
||||
onDeviceSelected: (DeviceItemUiModel) -> Unit,
|
||||
onAppSelected: (DeviceAppUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
DeviceSelector(
|
||||
state = devicesState,
|
||||
onDeviceSelected = onDeviceSelected,
|
||||
)
|
||||
|
||||
AnimatedVisibility(devicesState is DevicesStateUiModel.WithDevices) {
|
||||
DeviceAppSelector(
|
||||
devicesState = devicesState,
|
||||
appsState = appsState,
|
||||
onAppSelected = onAppSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceAppSelector(
|
||||
devicesState: DevicesStateUiModel,
|
||||
appsState: AppsStateUiModel,
|
||||
onAppSelected: (DeviceAppUiModel) -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
if (devicesState is DevicesStateUiModel.WithDevices) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = false },
|
||||
) {
|
||||
val modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
|
||||
appsState.appSelected?.let {
|
||||
DeviceAppName(
|
||||
deviceApp = it,
|
||||
onClick = { expanded = true },
|
||||
modifier = modifier,
|
||||
)
|
||||
} ?: run {
|
||||
Selector(
|
||||
onClick = { expanded = true },
|
||||
) {
|
||||
Text(
|
||||
text = "Select",
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (appsState) {
|
||||
AppsStateUiModel.Empty,
|
||||
AppsStateUiModel.Loading -> {
|
||||
// no op
|
||||
}
|
||||
|
||||
is AppsStateUiModel.WithApps -> {
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.exposedDropdownSize(),
|
||||
) {
|
||||
appsState.apps
|
||||
.fastForEach { app ->
|
||||
DeviceAppName(
|
||||
deviceApp = app,
|
||||
onClick = {
|
||||
onAppSelected(app)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceSelector(
|
||||
state: DevicesStateUiModel,
|
||||
onDeviceSelected: (DeviceItemUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = it
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
when (state) {
|
||||
DevicesStateUiModel.Empty -> Empty()
|
||||
DevicesStateUiModel.Loading -> Loading()
|
||||
is DevicesStateUiModel.WithDevices -> DeviceView(
|
||||
device = state.deviceSelected,
|
||||
onClick = {},
|
||||
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||
)
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
containerColor = FloconTheme.colorPalette.panel,
|
||||
shadowElevation = 0.dp,
|
||||
shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
|
||||
modifier = Modifier.exposedDropdownSize(),
|
||||
) {
|
||||
if (state is DevicesStateUiModel.WithDevices) {
|
||||
state.devices.forEach { device ->
|
||||
DeviceView(
|
||||
device = device,
|
||||
selected = state.deviceSelected.id == device.id,
|
||||
onClick = {
|
||||
onDeviceSelected(device)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Selector(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: Shape = RoundedCornerShape(12.dp),
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.then(
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.background(FloconTheme.colorPalette.panel)
|
||||
.clickable(enabled = enabled, onClick = onClick)
|
||||
.padding(contentPadding),
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
content()
|
||||
Image(
|
||||
imageVector = Icons.Outlined.KeyboardArrowDown,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.width(16.dp),
|
||||
colorFilter = ColorFilter.tint(FloconTheme.colorPalette.onSurface)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Empty() {
|
||||
Selector(
|
||||
onClick = {},
|
||||
) {
|
||||
Text(
|
||||
text = "No Devices Found",
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||
style = FloconTheme.typography.bodyMedium,
|
||||
color = FloconTheme.colorPalette.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Loading() {
|
||||
Selector(
|
||||
onClick = {},
|
||||
) {
|
||||
FloconCircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceView(
|
||||
device: DeviceItemUiModel,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
selected: Boolean = false,
|
||||
) {
|
||||
Selector(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(Res.drawable.smartphone),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp, end = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
alpha = if (device.isActive) 1f else 0.4f
|
||||
},
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = device.deviceName,
|
||||
color = FloconTheme.colorPalette.onPanel,
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
if (device.isActive.not()) {
|
||||
Text(
|
||||
text = "Disconnected",
|
||||
color = FloconTheme.colorPalette.onPanel,
|
||||
style = FloconTheme.typography.bodySmall.copy(
|
||||
fontSize = 10.sp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (selected)
|
||||
FloconIcon(
|
||||
imageVector = Icons.Outlined.Check,
|
||||
tint = FloconTheme.colorPalette.onPanel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeviceAppName(
|
||||
deviceApp: DeviceAppUiModel,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Selector(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AppImage(
|
||||
deviceApp = deviceApp,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = deviceApp.name,
|
||||
style = FloconTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = FloconTheme.colorPalette.onPanel,
|
||||
)
|
||||
Text(
|
||||
text = deviceApp.packageName,
|
||||
style = FloconTheme.typography.bodySmall,
|
||||
color = FloconTheme.colorPalette.onPanel.copy(alpha = 0.8f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppImage(
|
||||
deviceApp: DeviceAppUiModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val imageBitmap = remember(deviceApp.iconEncoded) {
|
||||
deviceApp.iconEncoded?.let { encoded ->
|
||||
try {
|
||||
val decodedBytes = Base64.decode(encoded) //, Base64.DEFAULT)
|
||||
Image.makeFromEncoded(decodedBytes).toComposeImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageBitmap != null) {
|
||||
Image(
|
||||
bitmap = imageBitmap,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
// Fallback : affiche une icône par défaut si iconEncoded est null ou invalide
|
||||
Image(
|
||||
painter = painterResource(Res.drawable.smartphone),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun DeviceViewPreview() {
|
||||
FloconTheme {
|
||||
DeviceView(
|
||||
device = previewDeviceItemUiModel(),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package io.github.openflocon.flocondesktop.main.ui.view
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.AppsStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceAppUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceItemUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DevicesStateUiModel
|
||||
|
||||
@Composable
|
||||
fun MainScreenTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
devicesState: DevicesStateUiModel,
|
||||
appsState: AppsStateUiModel,
|
||||
onDeviceSelected: (DeviceItemUiModel) -> Unit,
|
||||
onAppSelected: (DeviceAppUiModel) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
DeviceSelectorView(
|
||||
devicesState = devicesState,
|
||||
appsState = appsState,
|
||||
onDeviceSelected = onDeviceSelected,
|
||||
onAppSelected = onAppSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,52 +2,28 @@
|
|||
|
||||
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.heightIn
|
||||
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.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import flocondesktop.composeapp.generated.resources.Res
|
||||
import flocondesktop.composeapp.generated.resources.app_icon_small
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.AppsStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceAppUiModel
|
||||
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
|
||||
|
||||
val PanelMaxWidth = 275.dp
|
||||
|
|
@ -66,7 +42,6 @@ fun LeftPanelView(
|
|||
.background(FloconTheme.colorPalette.surface)
|
||||
.padding(horizontal = 12.dp, vertical = 16.dp),
|
||||
) {
|
||||
Title(expanded = expanded)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
MenuSection(
|
||||
items = state.sections,
|
||||
|
|
@ -83,40 +58,6 @@ fun LeftPanelView(
|
|||
}
|
||||
}
|
||||
|
||||
@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(),
|
||||
) {
|
||||
Text(
|
||||
text = "Flocon",
|
||||
fontSize = 32.sp,
|
||||
style = FloconTheme.typography.titleLarge.copy(
|
||||
color = FloconTheme.colorPalette.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.MenuSection(
|
||||
items: List<LeftPannelSection>,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package io.github.openflocon.flocondesktop.main.ui.view.topbar
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import flocondesktop.composeapp.generated.resources.Res
|
||||
import flocondesktop.composeapp.generated.resources.app_icon_small
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.AppsStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceAppUiModel
|
||||
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.view.TopBarDeviceAndAppView
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
fun MainScreenTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
devicesState: DevicesStateUiModel,
|
||||
appsState: AppsStateUiModel,
|
||||
onDeviceSelected: (DeviceItemUiModel) -> Unit,
|
||||
onAppSelected: (DeviceAppUiModel) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Title()
|
||||
Spacer(modifier = Modifier.width(18.dp))
|
||||
TopBarDeviceAndAppView(
|
||||
devicesState = devicesState,
|
||||
appsState = appsState,
|
||||
onDeviceSelected = onDeviceSelected,
|
||||
onAppSelected = onAppSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
painter = painterResource(Res.drawable.app_icon_small),
|
||||
contentDescription = "Description de mon image",
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Flocon",
|
||||
style = FloconTheme.typography.titleSmall.copy(
|
||||
fontSize = 18.sp,
|
||||
color = FloconTheme.colorPalette.onSurface,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
@file:Suppress("UnusedReceiverParameter")
|
||||
|
||||
package io.github.openflocon.flocondesktop.main.ui.view
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.AppsStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceAppUiModel
|
||||
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.view.topbar.app.TopBarAppDropdown
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.topbar.device.TopBarDeviceDropdown
|
||||
|
||||
@Composable
|
||||
internal fun TopBarDeviceAndAppView(
|
||||
devicesState: DevicesStateUiModel,
|
||||
appsState: AppsStateUiModel,
|
||||
onDeviceSelected: (DeviceItemUiModel) -> Unit,
|
||||
onAppSelected: (DeviceAppUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TopBarDeviceDropdown(
|
||||
state = devicesState,
|
||||
onDeviceSelected = onDeviceSelected,
|
||||
)
|
||||
|
||||
AnimatedVisibility(devicesState is DevicesStateUiModel.WithDevices) {
|
||||
TopBarAppDropdown(
|
||||
devicesState = devicesState,
|
||||
appsState = appsState,
|
||||
onAppSelected = onAppSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package io.github.openflocon.flocondesktop.main.ui.view.topbar
|
||||
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.KeyboardArrowDown
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
|
||||
@Composable
|
||||
internal fun TopBarSelector(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
shape: Shape = RoundedCornerShape(12.dp),
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.then(
|
||||
Modifier
|
||||
.clip(shape)
|
||||
.background(FloconTheme.colorPalette.panel)
|
||||
.clickable(enabled = enabled, onClick = onClick)
|
||||
.padding(contentPadding),
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
content()
|
||||
Image(
|
||||
imageVector = Icons.Outlined.KeyboardArrowDown,
|
||||
contentDescription = "",
|
||||
modifier = Modifier.width(16.dp),
|
||||
colorFilter = ColorFilter.tint(FloconTheme.colorPalette.onSurface)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.github.openflocon.flocondesktop.main.ui.view.topbar.app
|
||||
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.AppsStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceAppUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DevicesStateUiModel
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.topbar.TopBarAppView
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.topbar.TopBarSelector
|
||||
|
||||
|
||||
@Composable
|
||||
internal fun TopBarAppDropdown(
|
||||
devicesState: DevicesStateUiModel,
|
||||
appsState: AppsStateUiModel,
|
||||
onAppSelected: (DeviceAppUiModel) -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
if (devicesState is DevicesStateUiModel.WithDevices) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = false },
|
||||
) {
|
||||
val modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
|
||||
appsState.appSelected?.let {
|
||||
TopBarAppView(
|
||||
deviceApp = it,
|
||||
onClick = { expanded = true },
|
||||
modifier = modifier,
|
||||
)
|
||||
} ?: run {
|
||||
TopBarSelector(
|
||||
onClick = { expanded = true },
|
||||
) {
|
||||
Text(
|
||||
text = "Select",
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when (appsState) {
|
||||
AppsStateUiModel.Empty,
|
||||
AppsStateUiModel.Loading -> {
|
||||
// no op
|
||||
}
|
||||
|
||||
is AppsStateUiModel.WithApps -> {
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.exposedDropdownSize(),
|
||||
) {
|
||||
appsState.apps
|
||||
.fastForEach { app ->
|
||||
TopBarAppView(
|
||||
deviceApp = app,
|
||||
onClick = {
|
||||
onAppSelected(app)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package io.github.openflocon.flocondesktop.main.ui.view.topbar
|
||||
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import flocondesktop.composeapp.generated.resources.Res
|
||||
import flocondesktop.composeapp.generated.resources.smartphone
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceAppUiModel
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.skia.Image
|
||||
import kotlin.io.encoding.Base64
|
||||
|
||||
|
||||
@Composable
|
||||
internal fun TopBarAppView(
|
||||
deviceApp: DeviceAppUiModel,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
TopBarSelector(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AppImage(
|
||||
deviceApp = deviceApp,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = deviceApp.name,
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = FloconTheme.colorPalette.onPanel,
|
||||
)
|
||||
Text(
|
||||
text = deviceApp.packageName,
|
||||
style = FloconTheme.typography.bodySmall.copy(
|
||||
fontSize = 10.sp,
|
||||
),
|
||||
color = FloconTheme.colorPalette.onPanel.copy(alpha = 0.8f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun AppImage(
|
||||
deviceApp: DeviceAppUiModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val imageBitmap = remember(deviceApp.iconEncoded) {
|
||||
deviceApp.iconEncoded?.let { encoded ->
|
||||
try {
|
||||
val decodedBytes = Base64.decode(encoded) //, Base64.DEFAULT)
|
||||
Image.makeFromEncoded(decodedBytes).toComposeImageBitmap()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageBitmap != null) {
|
||||
Image(
|
||||
bitmap = imageBitmap,
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
// Fallback : affiche une icône par défaut si iconEncoded est null ou invalide
|
||||
Image(
|
||||
painter = painterResource(Res.drawable.smartphone),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package io.github.openflocon.flocondesktop.main.ui.view.topbar.device
|
||||
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.view.topbar.TopBarDeviceView
|
||||
import io.github.openflocon.flocondesktop.main.ui.view.topbar.TopBarSelector
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.FloconCircularProgressIndicator
|
||||
|
||||
@Composable
|
||||
internal fun TopBarDeviceDropdown(
|
||||
state: DevicesStateUiModel,
|
||||
onDeviceSelected: (DeviceItemUiModel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = it
|
||||
},
|
||||
modifier = modifier,
|
||||
) {
|
||||
when (state) {
|
||||
DevicesStateUiModel.Empty -> Empty()
|
||||
DevicesStateUiModel.Loading -> Loading()
|
||||
is DevicesStateUiModel.WithDevices -> TopBarDeviceView(
|
||||
device = state.deviceSelected,
|
||||
onClick = {},
|
||||
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),
|
||||
)
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
containerColor = FloconTheme.colorPalette.panel,
|
||||
shadowElevation = 0.dp,
|
||||
shape = RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp),
|
||||
modifier = Modifier.exposedDropdownSize(),
|
||||
) {
|
||||
if (state is DevicesStateUiModel.WithDevices) {
|
||||
state.devices.forEach { device ->
|
||||
TopBarDeviceView(
|
||||
device = device,
|
||||
selected = state.deviceSelected.id == device.id,
|
||||
onClick = {
|
||||
onDeviceSelected(device)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun Empty() {
|
||||
TopBarSelector(
|
||||
onClick = {},
|
||||
) {
|
||||
Text(
|
||||
text = "No Devices Found",
|
||||
modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp),
|
||||
style = FloconTheme.typography.bodyMedium,
|
||||
color = FloconTheme.colorPalette.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Loading() {
|
||||
TopBarSelector(
|
||||
onClick = {},
|
||||
) {
|
||||
FloconCircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package io.github.openflocon.flocondesktop.main.ui.view.topbar
|
||||
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import flocondesktop.composeapp.generated.resources.Res
|
||||
import flocondesktop.composeapp.generated.resources.smartphone
|
||||
import io.github.openflocon.flocondesktop.main.ui.model.DeviceItemUiModel
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.FloconIcon
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
@Composable
|
||||
internal fun TopBarDeviceView(
|
||||
device: DeviceItemUiModel,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
selected: Boolean = false,
|
||||
) {
|
||||
TopBarSelector(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(Res.drawable.smartphone),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp, end = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
alpha = if (device.isActive) 1f else 0.4f
|
||||
},
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = device.deviceName,
|
||||
color = FloconTheme.colorPalette.onPanel,
|
||||
style = FloconTheme.typography.bodySmall.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
if (device.isActive.not()) {
|
||||
Text(
|
||||
text = "Disconnected",
|
||||
color = FloconTheme.colorPalette.onPanel,
|
||||
style = FloconTheme.typography.bodySmall.copy(
|
||||
fontSize = 10.sp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (selected)
|
||||
FloconIcon(
|
||||
imageVector = Icons.Outlined.Check,
|
||||
tint = FloconTheme.colorPalette.onPanel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue