From 2876bf1ce4a7b7f5a5b80a01a08ddeeb03ee63a0 Mon Sep 17 00:00:00 2001 From: Florent CHAMPIGNY Date: Mon, 25 Aug 2025 14:05:26 +0200 Subject: [PATCH] =?UTF-8?q?refact:=20[DESIGN]=C2=A0moved=20togo=20in=20top?= =?UTF-8?q?=20bar=20(#156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Florent Champigny --- .../flocondesktop/main/ui/MainScreen.kt | 4 +- .../main/ui/view/DeviceSelectorView.kt | 396 ------------------ .../main/ui/view/MainScreenTopBar.kt | 33 -- .../main/ui/view/leftpannel/LeftPannelView.kt | 59 --- .../main/ui/view/topbar/MainScreenTopBar.kt | 78 ++++ .../ui/view/topbar/TopBarDeviceAndAppView.kt | 47 +++ .../main/ui/view/topbar/TopBarSelector.kt | 52 +++ .../ui/view/topbar/app/TopBarAppDropdown.kt | 83 ++++ .../main/ui/view/topbar/app/TopBarAppView.kt | 99 +++++ .../topbar/device/TopBarDeviceDropdown.kt | 96 +++++ .../ui/view/topbar/device/TopBarDeviceView.kt | 85 ++++ 11 files changed, 541 insertions(+), 491 deletions(-) delete mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/DeviceSelectorView.kt delete mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/MainScreenTopBar.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/MainScreenTopBar.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/TopBarDeviceAndAppView.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/TopBarSelector.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/app/TopBarAppDropdown.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/app/TopBarAppView.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceDropdown.kt create mode 100644 FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceView.kt diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/MainScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/MainScreen.kt index f45c2136..95148b57 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/MainScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/MainScreen.kt @@ -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 diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/DeviceSelectorView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/DeviceSelectorView.kt deleted file mode 100644 index 02d74b0a..00000000 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/DeviceSelectorView.kt +++ /dev/null @@ -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 = {}, - ) - } -} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/MainScreenTopBar.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/MainScreenTopBar.kt deleted file mode 100644 index 8cc7fe5e..00000000 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/MainScreenTopBar.kt +++ /dev/null @@ -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, - ) - } -} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/leftpannel/LeftPannelView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/leftpannel/LeftPannelView.kt index 531275bc..ee03f556 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/leftpannel/LeftPannelView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/leftpannel/LeftPannelView.kt @@ -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, diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/MainScreenTopBar.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/MainScreenTopBar.kt new file mode 100644 index 00000000..20bf7e49 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/MainScreenTopBar.kt @@ -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, + ), + ) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/TopBarDeviceAndAppView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/TopBarDeviceAndAppView.kt new file mode 100644 index 00000000..4289fd50 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/TopBarDeviceAndAppView.kt @@ -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, + ) + } + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/TopBarSelector.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/TopBarSelector.kt new file mode 100644 index 00000000..9c80347c --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/TopBarSelector.kt @@ -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) + ) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/app/TopBarAppDropdown.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/app/TopBarAppDropdown.kt new file mode 100644 index 00000000..b3b538ef --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/app/TopBarAppDropdown.kt @@ -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 + }, + ) + } + } + } + } + } + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/app/TopBarAppView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/app/TopBarAppView.kt new file mode 100644 index 00000000..b38ffc7c --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/app/TopBarAppView.kt @@ -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, + ) + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceDropdown.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceDropdown.kt new file mode 100644 index 00000000..371c4945 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceDropdown.kt @@ -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() + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceView.kt new file mode 100644 index 00000000..1d6eb04d --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/main/ui/view/topbar/device/TopBarDeviceView.kt @@ -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, + ) + } + } + } +}