refact: [DESIGN] moved togo in top bar (#156)

Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
Florent CHAMPIGNY 2025-08-25 14:05:26 +02:00 committed by GitHub
parent 1520fd52cd
commit 2876bf1ce4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 541 additions and 491 deletions

View file

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

View file

@ -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 = {},
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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