refact: [DASHBOARD] display list on left (#467)
Some checks are pending
docs / deploy (push) Waiting to run

Co-authored-by: Florent Champigny <florent@bere.al>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Florent CHAMPIGNY 2025-12-24 10:16:39 +01:00 committed by GitHub
parent 4a458ccb90
commit 8e74ae689a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 238 additions and 173 deletions

View file

@ -6,6 +6,7 @@
<string name="close">Close</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
<string name="crashes">Crashes</string>
<string name="dashboards">Dashboards</string>
<string name="dashboard_action_delete">Delete Dashboards</string>
<string name="dashboard_empty">No dashboard</string>
<string name="dashboard_removed">Dashboard removed</string>

View file

@ -13,6 +13,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.dashboard_action_delete
@ -78,50 +84,60 @@ fun DashboardScreen(
FloconFeature(
modifier = modifier
) {
FloconPageTopBar(
modifier = Modifier.fillMaxWidth(),
selector = {
DashboardSelectorView(
dashboardsState = deviceDashboards,
Column(Modifier.fillMaxSize()) {
FloconPageTopBar(
modifier = Modifier.fillMaxWidth(),
selector = null,
filterBar = {
Box(modifier = Modifier.weight(1f)) // to have actions on the right
},
actions = {
DashboardArrangementView(
onArrangementClicked = onArrangementClicked,
arrangement = arrangement
)
FloconOverflow {
FloconDropdownMenuItem(
text = stringResource(Res.string.dashboard_action_delete),
leadingIcon = Icons.Outlined.Delete,
onClick = { deleteCurrentDashboard() }
)
}
}
)
Spacer(modifier = Modifier.height(8.dp))
Row(Modifier.fillMaxSize()) {
DashboardSidebarView(
modifier = Modifier
.fillMaxHeight()
.width(340.dp),
state = deviceDashboards,
onDashboardSelected = onDashboardSelected,
modifier = Modifier.fillMaxWidth(),
onDeleteClicked = onDeleteClicked,
)
},
filterBar = {
Box(modifier = Modifier.weight(1f)) // to have actions on the right
},
actions = {
DashboardArrangementView(
onArrangementClicked = onArrangementClicked,
arrangement = arrangement
)
FloconOverflow {
FloconDropdownMenuItem(
text = stringResource(Res.string.dashboard_action_delete),
leadingIcon = Icons.Outlined.Delete,
onClick = { deleteCurrentDashboard() }
Spacer(modifier = Modifier.width(12.dp))
state?.let {
DashboardView(
modifier = Modifier
.fillMaxSize()
.clip(FloconTheme.shapes.medium)
.background(FloconTheme.colorPalette.primary)
.padding(all = 8.dp),
viewState = state,
onClickButton = onClickButton,
submitTextField = submitTextField,
submitForm = submitForm,
onUpdateCheckBox = onUpdateCheckBox,
arrangement = arrangement,
onOpenExternalClicked = onOpenExternalClicked,
)
}
}
)
state?.let {
DashboardView(
modifier = Modifier
.fillMaxSize()
.clip(FloconTheme.shapes.medium)
.background(FloconTheme.colorPalette.primary)
.padding(all = 8.dp),
viewState = state,
onClickButton = onClickButton,
submitTextField = submitTextField,
submitForm = submitForm,
onUpdateCheckBox = onUpdateCheckBox,
arrangement = arrangement,
onOpenExternalClicked = onOpenExternalClicked,
)
}
}
}

View file

@ -1,136 +0,0 @@
package io.github.openflocon.flocondesktop.features.dashboard.view
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.KeyboardArrowDown
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.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.dashboard_empty
import io.github.openflocon.flocondesktop.features.dashboard.model.DashboardsStateUiModel
import io.github.openflocon.flocondesktop.features.dashboard.model.DeviceDashboardUiModel
import io.github.openflocon.library.designsystem.FloconTheme
import io.github.openflocon.library.designsystem.components.DropdownFilterFieldView
import io.github.openflocon.library.designsystem.components.FloconButton
import io.github.openflocon.library.designsystem.components.FloconDropdownMenu
import io.github.openflocon.library.designsystem.components.FloconDropdownMenuItem
import io.github.openflocon.library.designsystem.components.FloconIcon
import io.github.openflocon.library.designsystem.components.FloconLinearProgressIndicator
import org.jetbrains.compose.resources.stringResource
@Composable
internal fun DashboardSelectorView(
dashboardsState: DashboardsStateUiModel,
onDashboardSelected: (DeviceDashboardUiModel) -> Unit,
onDeleteClicked: (DeviceDashboardUiModel) -> Unit,
modifier: Modifier = Modifier,
) {
var expanded by remember { mutableStateOf(false) }
FloconDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
onExpandRequest = {
if (dashboardsState is DashboardsStateUiModel.WithContent) expanded = true
},
anchorContent = {
FloconButton(
onClick = {
if (dashboardsState is DashboardsStateUiModel.WithContent) expanded = true
}, containerColor = FloconTheme.colorPalette.secondary
) {
when (dashboardsState) {
DashboardsStateUiModel.Empty -> Text(
stringResource(Res.string.dashboard_empty),
style = FloconTheme.typography.bodySmall
)
DashboardsStateUiModel.Loading -> FloconLinearProgressIndicator()
is DashboardsStateUiModel.WithContent -> {
Text(
text = dashboardsState.selected.id,
style = FloconTheme.typography.bodySmall
)
FloconIcon(
imageVector = Icons.Outlined.KeyboardArrowDown,
modifier = Modifier.size(16.dp),
)
}
}
}
},
modifier = modifier
) {
var filterText by remember { mutableStateOf("") }
DropdownFilterFieldView(
value = filterText,
onValueChanged = {
filterText = it
},
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
if (dashboardsState is DashboardsStateUiModel.WithContent) {
val filteredItems = remember(filterText, dashboardsState.dashboards) {
dashboardsState.dashboards.filter {
it.id.contains(
filterText,
ignoreCase = true
)
}
}
filteredItems.fastForEach { dashboard ->
FloconDropdownMenuItem(
text = dashboard.id,
onClick = {
onDashboardSelected(dashboard)
expanded = false
},
secondaryAction = {
Box(
Modifier.clip(RoundedCornerShape(4.dp))
.background(
Color.White.copy(alpha = 0.8f)
).padding(2.dp).clickable {
onDeleteClicked(dashboard)
},
contentAlignment = Alignment.Center,
) {
FloconIcon(
imageVector = Icons.Outlined.Close,
tint = FloconTheme.colorPalette.primary,
modifier = Modifier.size(14.dp)
)
}
}
)
}
}
}
}
}

View file

@ -0,0 +1,184 @@
package io.github.openflocon.flocondesktop.features.dashboard.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Dashboard
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.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.composeunstyled.Text
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.dashboards
import flocondesktop.composeapp.generated.resources.filter
import io.github.openflocon.flocondesktop.features.dashboard.model.DashboardsStateUiModel
import io.github.openflocon.flocondesktop.features.dashboard.model.DeviceDashboardUiModel
import io.github.openflocon.flocondesktop.features.network.list.view.components.FilterBar
import io.github.openflocon.library.designsystem.FloconTheme
import io.github.openflocon.library.designsystem.components.FloconIcon
import org.jetbrains.compose.resources.stringResource
@Composable
fun DashboardSidebarView(
modifier: Modifier = Modifier,
state: DashboardsStateUiModel,
onDashboardSelected: (DeviceDashboardUiModel) -> Unit,
onDeleteClicked: (DeviceDashboardUiModel) -> Unit,
) {
val borderColor = FloconTheme.colorPalette.secondary
Surface(
color = FloconTheme.colorPalette.primary,
modifier = modifier
.clip(FloconTheme.shapes.medium)
.border(
width = 1.dp,
color = borderColor,
shape = FloconTheme.shapes.medium
)
) {
Column(
Modifier.fillMaxSize()
) {
Column(
Modifier.fillMaxWidth()
.weight(1f)
.verticalScroll(rememberScrollState())
.padding(all = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
stringResource(Res.string.dashboards),
color = FloconTheme.colorPalette.onSurface,
style = FloconTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Bold,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp)
)
when (state) {
DashboardsStateUiModel.Empty -> Unit
DashboardsStateUiModel.Loading -> Unit
is DashboardsStateUiModel.WithContent -> {
var filterText = remember { mutableStateOf("") }
val filteredDashboards = remember(filterText.value, state.dashboards) {
val filterTextValue = filterText.value
if (filterTextValue.isBlank()) {
state.dashboards
} else {
state.dashboards.filter {
it.id.contains(filterTextValue, ignoreCase = true)
}
}
}
FilterBar(
filterText = filterText,
placeholderText = stringResource(Res.string.filter),
onTextChange = {
filterText.value = it
},
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).padding(bottom = 12.dp),
)
filteredDashboards.forEach {
DashboardItemView(
dashboard = it,
onSelect = {
onDashboardSelected(it)
},
onDelete = {
onDeleteClicked(it)
},
modifier = Modifier.fillMaxWidth(),
isSelected = it.id == state.selected.id
)
}
}
}
}
}
}
}
@Composable
private fun DashboardItemView(
dashboard: DeviceDashboardUiModel,
isSelected: Boolean,
onSelect: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
val (background, textColor) = if (isSelected) {
FloconTheme.colorPalette.accent.copy(alpha = 0.4f) to FloconTheme.colorPalette.onAccent
} else {
Color.Transparent to FloconTheme.colorPalette.onSurface
}
Row(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.background(background)
.clickable(onClick = onSelect)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier.width(14.dp),
imageVector = Icons.Outlined.Dashboard,
contentDescription = null,
colorFilter = ColorFilter.tint(textColor),
)
Spacer(modifier = Modifier.width(4.dp))
Text(
dashboard.id,
color = textColor,
style = FloconTheme.typography.bodyMedium,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Box(
Modifier.clip(RoundedCornerShape(4.dp))
.background(
Color.White.copy(alpha = 0.1f)
).clickable {
onDelete()
}.padding(2.dp),
contentAlignment = Alignment.Center,
) {
FloconIcon(
imageVector = Icons.Outlined.Close,
tint = textColor.copy(alpha = 0.6f),
modifier = Modifier.size(14.dp)
)
}
}
}