Network Filters #20 (#27)

* feature: Add filters

* fix: Use adbPath set

* fix: Remove old method

* fix: Remove state flow

* featrure: Rework methods

* feature: (start) Rework network screen

* feature: Add molecule

* feature: Migrate

* fix: Merge

* feature: Rework

* feature: Filter methods

* fix: Clean animation

* feature: Add minimum size

* fix: Remove molecule

* fix: NetworkUiState default params

* fix: Discussion

* fix: Filter

---------

Co-authored-by: Raphael Teyssandier <rteyssandier@sephora.fr>
This commit is contained in:
Raphael Teyssandier 2025-08-04 14:40:54 +02:00 committed by GitHub
parent 6f9c6c7228
commit 72d3939858
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 577 additions and 237 deletions

View file

@ -22,6 +22,7 @@ kotlin {
// Pour Kotlin 1.9+
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
freeCompilerArgs.add("-Xcontext-parameters")
}
@OptIn(ExperimentalKotlinGradlePluginApi::class)

View file

@ -0,0 +1,31 @@
package io.github.openflocon.flocondesktop.common.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
interface ViewModelEvent<E> {
val events: SharedFlow<E>
fun ViewModel.sendEvents(vararg event: E)
interface Event
}
class ViewModelEventImpl<E : ViewModelEvent.Event> : ViewModelEvent<E> {
private val _events = MutableSharedFlow<E>()
override val events: SharedFlow<E> = _events.asSharedFlow()
override fun ViewModel.sendEvents(vararg event: E) {
viewModelScope.launch {
event.forEach { _events.emit(it) }
}
}
}

View file

@ -0,0 +1,12 @@
package io.github.openflocon.flocondesktop.features.network.ui
import androidx.compose.runtime.Immutable
@Immutable
data class ContentUiState(
val selectedRequestId: String?
)
fun previewContentUiState() = ContentUiState(
selectedRequestId = null
)

View file

@ -0,0 +1,15 @@
package io.github.openflocon.flocondesktop.features.network.ui
import androidx.compose.runtime.Immutable
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkMethodUi
@Immutable
data class FilterUiState(
val query: String,
val methods: List<NetworkMethodUi>
)
fun previewFilterUiState() = FilterUiState(
query = "",
methods = emptyList()
)

View file

@ -0,0 +1,29 @@
package io.github.openflocon.flocondesktop.features.network.ui
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkItemViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkMethodUi
import io.github.openflocon.flocondesktop.features.network.ui.view.filters.MethodFilter
sealed interface NetworkAction {
data class SelectRequest(val id: String) : NetworkAction
data class CopyText(val text: String) : NetworkAction
data object ClosePanel : NetworkAction
data object Reset : NetworkAction
data class CopyUrl(val item: NetworkItemViewState) : NetworkAction
data class CopyCUrl(val item: NetworkItemViewState) : NetworkAction
data class Remove(val item: NetworkItemViewState) : NetworkAction
data class RemoveLinesAbove(val item: NetworkItemViewState) : NetworkAction
data class FilterQuery(val query: String) : NetworkAction
data class FilterMethod(val method: NetworkMethodUi, val add: Boolean) : NetworkAction
}

View file

@ -0,0 +1,5 @@
package io.github.openflocon.flocondesktop.features.network.ui
import io.github.openflocon.flocondesktop.common.ui.ViewModelEvent
sealed interface NetworkEvent : ViewModelEvent.Event

View file

@ -0,0 +1,20 @@
package io.github.openflocon.flocondesktop.features.network.ui
import androidx.compose.runtime.Immutable
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkDetailViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkItemViewState
@Immutable
data class NetworkUiState(
val items: List<NetworkItemViewState>,
val contentState: ContentUiState,
val detailState: NetworkDetailViewState?,
val filterState: FilterUiState
)
fun previewNetworkUiState() = NetworkUiState(
items = emptyList(),
detailState = null,
contentState = previewContentUiState(),
filterState = previewFilterUiState()
)

View file

@ -15,10 +15,12 @@ import io.github.openflocon.flocondesktop.features.network.ui.mapper.toDetailUi
import io.github.openflocon.flocondesktop.features.network.ui.mapper.toUi
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkDetailViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkItemViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.OnNetworkItemUserAction
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkMethodUi
import io.github.openflocon.flocondesktop.features.network.ui.view.filters.MethodFilter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
@ -40,85 +42,158 @@ class NetworkViewModel(
private val feedbackDisplayer: FeedbackDisplayer,
) : ViewModel() {
val state: StateFlow<List<NetworkItemViewState>> =
observeHttpRequestsUseCase()
.map { list -> list.map { toUi(it) } }
.flowOn(dispatcherProvider.viewModel)
.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(5_000), emptyList())
private val filterMethod = MethodFilter()
private val clickedRequestId = MutableStateFlow<String?>(null)
private val contentState = MutableStateFlow(ContentUiState(selectedRequestId = null))
private val filterUiState = MutableStateFlow(FilterUiState(query = "", methods = NetworkMethodUi.all()))
val detailState: StateFlow<NetworkDetailViewState?> =
clickedRequestId
private val detailState: StateFlow<NetworkDetailViewState?> =
contentState.map { it.selectedRequestId }
.flatMapLatest { id ->
if (id == null) {
flowOf(null)
} else {
observeHttpRequestsByIdUseCase(id)
.distinctUntilChanged()
.map {
it?.let {
toDetailUi(it)
}
}
.map { it?.let { toDetailUi(it) } }
}
}
.flowOn(dispatcherProvider.viewModel)
.stateIn(viewModelScope, started = SharingStarted.WhileSubscribed(5_000), null)
fun onNetworkItemUserAction(action: OnNetworkItemUserAction) {
viewModelScope.launch(dispatcherProvider.viewModel) {
when (action) {
is OnNetworkItemUserAction.CopyCUrl -> {
val domainModel = observeHttpRequestsByIdUseCase(action.item.uuid).firstOrNull()
?: return@launch
val curl = generateCurlCommandUseCase(domainModel)
copyToClipboard(curl)
}
private val filteredItems = combine(
observeHttpRequestsUseCase().map { list -> list.map { toUi(it) } },
filterUiState
) { items, filterState ->
filterItems(items, filterState)
}
.distinctUntilChanged()
is OnNetworkItemUserAction.CopyUrl -> {
val domainModel = observeHttpRequestsByIdUseCase(action.item.uuid).firstOrNull()
?: return@launch
copyToClipboard(domainModel.url)
}
val uiState = combine(
filteredItems,
contentState,
detailState,
filterUiState
) { items, content, detail, filter ->
NetworkUiState(
items = items,
contentState = content,
detailState = detail,
filterState = filter
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NetworkUiState(
items = emptyList(),
detailState = detailState.value,
contentState = contentState.value,
filterState = filterUiState.value
)
)
is OnNetworkItemUserAction.OnClicked -> {
clickedRequestId.update {
if (it == action.item.uuid) {
null
} else {
action.item.uuid
}
}
}
is OnNetworkItemUserAction.Remove -> {
removeHttpRequestUseCase(requestId = action.item.uuid)
}
is OnNetworkItemUserAction.RemoveLinesAbove -> {
removeHttpRequestsBeforeUseCase(requestId = action.item.uuid)
}
}
fun onAction(action: NetworkAction) {
when (action) {
is NetworkAction.SelectRequest -> onSelectRequest(action)
NetworkAction.ClosePanel -> onClosePanel()
is NetworkAction.CopyText -> onCopyText(action)
NetworkAction.Reset -> onReset()
is NetworkAction.CopyCUrl -> onCopyCUrl(action)
is NetworkAction.CopyUrl -> onCopyUrl(action)
is NetworkAction.Remove -> onRemove(action)
is NetworkAction.RemoveLinesAbove -> onRemoveLinesAbove(action)
is NetworkAction.FilterQuery -> onFilterQuery(action)
is NetworkAction.FilterMethod -> onFilterMethod(action)
}
}
fun onCopyText(text: String) {
viewModelScope.launch(dispatcherProvider.viewModel) {
copyToClipboard(text)
feedbackDisplayer.displayMessage("copied")
private fun onSelectRequest(action: NetworkAction.SelectRequest) {
contentState.update { state ->
state.copy(
selectedRequestId = if (state.selectedRequestId == action.id) {
null
} else {
action.id
}
)
}
}
fun closeDetailPanel() {
viewModelScope.launch(dispatcherProvider.viewModel) {
clickedRequestId.update { null }
}
private fun onClosePanel() {
contentState.update { it.copy(selectedRequestId = null) }
}
fun onReset() {
private fun onCopyText(action: NetworkAction.CopyText) {
copyToClipboard(action.text)
feedbackDisplayer.displayMessage("copied")
}
private fun onReset() {
viewModelScope.launch(dispatcherProvider.viewModel) {
resetCurrentDeviceHttpRequestsUseCase()
}
}
private fun onCopyCUrl(action: NetworkAction.CopyCUrl) {
viewModelScope.launch(dispatcherProvider.viewModel) {
val domainModel = observeHttpRequestsByIdUseCase(action.item.uuid).firstOrNull()
?: return@launch
val curl = generateCurlCommandUseCase(domainModel)
copyToClipboard(curl)
}
}
private fun onCopyUrl(action: NetworkAction.CopyUrl) {
viewModelScope.launch(dispatcherProvider.viewModel) {
val domainModel = observeHttpRequestsByIdUseCase(action.item.uuid).firstOrNull()
?: return@launch
copyToClipboard(domainModel.url)
}
}
private fun onRemove(action: NetworkAction.Remove) {
viewModelScope.launch(dispatcherProvider.viewModel) {
removeHttpRequestUseCase(requestId = action.item.uuid)
}
}
private fun onRemoveLinesAbove(action: NetworkAction.RemoveLinesAbove) {
viewModelScope.launch(dispatcherProvider.viewModel) {
removeHttpRequestsBeforeUseCase(requestId = action.item.uuid)
}
}
private fun onFilterQuery(action: NetworkAction.FilterQuery) {
filterUiState.update { state ->
state.copy(query = action.query)
}
}
private fun onFilterMethod(action: NetworkAction.FilterMethod) {
filterUiState.update { state ->
state.copy(
methods = if (action.add) {
state.methods + action.method
} else {
state.methods - action.method
}
)
}
}
private fun filterItems(
items: List<NetworkItemViewState>,
filterState: FilterUiState
): List<NetworkItemViewState> {
var filteredItems = items
if (filterState.query.isNotEmpty())
filteredItems = filteredItems.filter { it.contains(filterState.query) }
if (filterState.methods.isNotEmpty())
filteredItems = filterMethod.filter(filterState, filteredItems)
return filteredItems
}
}

View file

@ -12,6 +12,7 @@ fun toTypeUi(networkRequest: FloconHttpRequestDomainModel): NetworkItemViewState
val query = extractPath(networkRequest.url)
NetworkItemViewState.NetworkTypeUi.Url(
query = query,
method = networkRequest.request.method
)
}

View file

@ -27,6 +27,7 @@ data class NetworkItemViewState(
@Immutable
data class Url(
val method: String,
val query: String,
) : NetworkTypeUi {
override fun contains(text: String): Boolean = query.contains(text, ignoreCase = true)
@ -59,6 +60,7 @@ fun previewNetworkItemViewState(): NetworkItemViewState = NetworkItemViewState(
status = NetworkStatusUi("200", true),
type = NetworkItemViewState.NetworkTypeUi.Url(
query = "/search?q=test",
method = "get"
),
)

View file

@ -38,6 +38,7 @@ sealed interface NetworkMethodUi {
override val text: String = "QUERY"
override val icon = Res.drawable.graphql
}
data object MUTATION : GraphQl {
override val text: String = "MUTATION"
override val icon = Res.drawable.graphql
@ -52,5 +53,22 @@ sealed interface NetworkMethodUi {
data class OTHER(
override val text: String,
override val icon: DrawableResource?,
) : NetworkMethodUi
) : NetworkMethodUi {
companion object {
val EMPTY = OTHER(text = "", icon = null)
}
}
companion object {
fun all() = listOf(
Http.GET,
Http.POST,
Http.PUT,
Http.DELETE,
GraphQl.QUERY,
GraphQl.MUTATION,
Grpc,
OTHER.EMPTY
)
}
}

View file

@ -1,23 +0,0 @@
package io.github.openflocon.flocondesktop.features.network.ui.model
sealed interface OnNetworkItemUserAction {
data class OnClicked(
val item: NetworkItemViewState,
) : OnNetworkItemUserAction
data class CopyUrl(
val item: NetworkItemViewState,
) : OnNetworkItemUserAction
data class CopyCUrl(
val item: NetworkItemViewState,
) : OnNetworkItemUserAction
data class Remove(
val item: NetworkItemViewState,
) : OnNetworkItemUserAction
data class RemoveLinesAbove(
val item: NetworkItemViewState,
) : OnNetworkItemUserAction
}

View file

@ -22,8 +22,8 @@ import androidx.compose.ui.unit.sp
import io.github.openflocon.flocondesktop.common.ui.ContextualItem
import io.github.openflocon.flocondesktop.common.ui.ContextualView
import io.github.openflocon.flocondesktop.common.ui.FloconColors
import io.github.openflocon.flocondesktop.features.network.ui.NetworkAction
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkItemViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.OnNetworkItemUserAction
import io.github.openflocon.flocondesktop.features.network.ui.model.previewNetworkItemViewState
import io.github.openflocon.flocondesktop.features.network.ui.view.components.MethodView
import io.github.openflocon.flocondesktop.features.network.ui.view.components.StatusView
@ -47,9 +47,9 @@ data class NetworkItemColumnWidths(
@Composable
fun NetworkItemView(
state: NetworkItemViewState,
columnWidths: NetworkItemColumnWidths = NetworkItemColumnWidths(), // Default widths provided
onUserAction: (OnNetworkItemUserAction) -> Unit,
onAction: (NetworkAction) -> Unit,
modifier: Modifier = Modifier,
columnWidths: NetworkItemColumnWidths = NetworkItemColumnWidths(), // Default widths provided
) {
// Use FloconTheme.typography for consistent text sizes
val bodySmall = FloconTheme.typography.bodySmall.copy(fontSize = 11.sp)
@ -76,10 +76,10 @@ fun NetworkItemView(
),
onSelect = {
when (it.id) {
"copy_url" -> onUserAction(OnNetworkItemUserAction.CopyUrl(state))
"copy_curl" -> onUserAction(OnNetworkItemUserAction.CopyCUrl(state))
"remove" -> onUserAction(OnNetworkItemUserAction.Remove(state))
"remove_lines_above" -> onUserAction(OnNetworkItemUserAction.RemoveLinesAbove(state))
"copy_url" -> onAction(NetworkAction.CopyUrl(state))
"copy_curl" -> onAction(NetworkAction.CopyCUrl(state))
"remove" -> onAction(NetworkAction.Remove(state))
"remove_lines_above" -> onAction(NetworkAction.RemoveLinesAbove(state))
}
},
) {
@ -87,9 +87,7 @@ fun NetworkItemView(
modifier = modifier
.padding(vertical = 4.dp)
.clip(shape = RoundedCornerShape(8.dp))
.clickable(onClick = {
onUserAction(OnNetworkItemUserAction.OnClicked(state))
})
.clickable(onClick = { onAction(NetworkAction.SelectRequest(state.uuid)) })
.padding(horizontal = 8.dp, vertical = 6.dp),
// Inner padding for content
verticalAlignment = Alignment.CenterVertically,
@ -205,7 +203,7 @@ private fun ItemViewPreview() {
NetworkItemView(
modifier = Modifier.fillMaxWidth(),
state = previewNetworkItemViewState(),
onUserAction = {},
onAction = {}
)
}
}

View file

@ -1,5 +1,8 @@
package io.github.openflocon.flocondesktop.features.network.ui.view
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -8,29 +11,28 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
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.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.github.openflocon.flocondesktop.common.ui.FloconColors
import io.github.openflocon.flocondesktop.common.ui.FloconTheme
import io.github.openflocon.flocondesktop.features.network.ui.NetworkAction
import io.github.openflocon.flocondesktop.features.network.ui.NetworkUiState
import io.github.openflocon.flocondesktop.features.network.ui.NetworkViewModel
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkDetailViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkItemViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.OnNetworkItemUserAction
import io.github.openflocon.flocondesktop.features.network.ui.model.previewGraphQlItemViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.previewNetworkItemViewState
import io.github.openflocon.flocondesktop.features.network.ui.view.components.NetworkFilterBar
import io.github.openflocon.flocondesktop.features.network.ui.previewNetworkUiState
import io.github.openflocon.flocondesktop.features.network.ui.view.header.NetworkFilter
import io.github.openflocon.flocondesktop.features.network.ui.view.components.NetworkItemHeaderView
import io.github.openflocon.library.designsystem.FloconTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
@ -39,34 +41,24 @@ import org.koin.compose.viewmodel.koinViewModel
@Composable
fun NetworkScreen(modifier: Modifier = Modifier) {
val viewModel: NetworkViewModel = koinViewModel()
val items by viewModel.state.collectAsStateWithLifecycle()
val detailState by viewModel.detailState.collectAsStateWithLifecycle()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
NetworkScreen(
networkItems = items,
modifier = modifier,
detailState = detailState,
onNetworkItemUserAction = viewModel::onNetworkItemUserAction,
onCopyText = viewModel::onCopyText,
onReset = viewModel::onReset,
closeDetailPanel = viewModel::closeDetailPanel,
uiState = uiState,
onAction = viewModel::onAction,
modifier = modifier
)
}
@Composable
fun NetworkScreen(
networkItems: List<NetworkItemViewState>,
detailState: NetworkDetailViewState?,
onNetworkItemUserAction: (OnNetworkItemUserAction) -> Unit,
onCopyText: (String) -> Unit,
closeDetailPanel: () -> Unit,
onReset: () -> Unit,
modifier: Modifier = Modifier,
uiState: NetworkUiState,
onAction: (NetworkAction) -> Unit,
modifier: Modifier = Modifier
) {
val columnWidths: NetworkItemColumnWidths =
remember { NetworkItemColumnWidths() } // Default widths provided
var filteredItems by remember { mutableStateOf<List<NetworkItemViewState>>(emptyList()) }
Surface(modifier = modifier) {
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
@ -79,17 +71,13 @@ fun NetworkScreen(
style = FloconTheme.typography.titleLarge,
color = FloconTheme.colorScheme.onSurface,
)
NetworkFilterBar(
modifier =
Modifier
NetworkFilter(
uiState = uiState,
modifier = Modifier
.fillMaxWidth()
.background(FloconColors.pannel)
.padding(horizontal = 12.dp),
networkItems = networkItems,
onResetClicked = onReset,
onItemsChange = {
filteredItems = it
},
onAction = onAction
)
NetworkItemHeaderView(
columnWidths = columnWidths,
@ -101,37 +89,53 @@ fun NetworkScreen(
) {
LazyColumn(
modifier =
Modifier
.fillMaxSize()
.clickable(
interactionSource = null,
indication = null,
enabled = detailState != null,
) {
closeDetailPanel()
},
Modifier
.fillMaxSize()
.clickable(
interactionSource = null,
indication = null,
enabled = uiState.detailState != null,
onClick = { onAction(NetworkAction.ClosePanel) }
),
) {
items(filteredItems) {
items(
items = uiState.items,
key = NetworkItemViewState::uuid
) {
NetworkItemView(
state = it,
columnWidths = columnWidths,
modifier = Modifier.fillMaxWidth(),
onUserAction = onNetworkItemUserAction,
onAction = onAction,
modifier = Modifier
.fillMaxWidth()
.animateItem()
)
}
}
}
}
detailState?.let {
NetworkDetailView(
modifier =
Modifier
.align(Alignment.TopEnd)
.fillMaxHeight()
.width(500.dp),
state = it,
onCopy = onCopyText,
)
AnimatedContent(
targetState = uiState.detailState,
transitionSpec = {
slideIntoContainer(SlideDirection.Start)
.togetherWith(slideOutOfContainer(SlideDirection.End))
},
contentKey = { it != null },
contentAlignment = Alignment.TopEnd,
modifier = Modifier
.fillMaxHeight()
.requiredWidth(500.dp)
.align(Alignment.TopEnd)
) {
if (it != null) {
NetworkDetailView(
modifier = Modifier.fillMaxSize(),
state = it,
onCopy = { onAction(NetworkAction.CopyText(it)) },
)
} else {
Box(Modifier.fillMaxSize())
}
}
}
}
@ -141,8 +145,8 @@ fun NetworkScreen(
@Preview
private fun NetworkScreenPreview() {
FloconTheme {
val networkItems =
remember {
val uiState = previewNetworkUiState().copy(
items = remember {
listOf(
previewNetworkItemViewState(),
previewNetworkItemViewState(),
@ -152,13 +156,11 @@ private fun NetworkScreenPreview() {
previewNetworkItemViewState(),
)
}
)
NetworkScreen(
networkItems = networkItems,
detailState = null,
closeDetailPanel = {},
onNetworkItemUserAction = {},
onCopyText = {},
onReset = {},
uiState = uiState,
onAction = {}
)
}
}

View file

@ -0,0 +1,47 @@
package io.github.openflocon.flocondesktop.features.network.ui.view.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.size
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.Icon
import androidx.compose.material3.SuggestionChip
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.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun FilterDropdown(
text: String,
icon: ImageVector?,
content: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit
) {
var filterExpanded by remember { mutableStateOf(false) }
Column {
SuggestionChip(
onClick = { filterExpanded = !filterExpanded },
label = { Text(text = text) },
icon = {
if (icon != null)
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
}
)
DropdownMenu(
expanded = filterExpanded,
onDismissRequest = { filterExpanded = false }
) {
content({ filterExpanded = false })
}
}
}

View file

@ -0,0 +1,12 @@
package io.github.openflocon.flocondesktop.features.network.ui.view.filters
import androidx.compose.runtime.Stable
import io.github.openflocon.flocondesktop.features.network.ui.FilterUiState
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkItemViewState
@Stable
interface Filters {
fun filter(state: FilterUiState, list: List<NetworkItemViewState>): List<NetworkItemViewState>
}

View file

@ -0,0 +1,22 @@
package io.github.openflocon.flocondesktop.features.network.ui.view.filters
import androidx.compose.runtime.Composable
import io.github.openflocon.flocondesktop.features.network.domain.model.FloconHttpRequestDomainModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
//class HostFilter(
// private val hosts: List<String>
//) : Filters {
// override val sort: Int
// get() = 1
//
// override val content: @Composable (() -> Unit) = {
//
// }
//
// override fun filter(list: List<FloconHttpRequestDomainModel>): Flow<List<FloconHttpRequestDomainModel>> {
// return flowOf(list)
// }
//
//}

View file

@ -0,0 +1,75 @@
package io.github.openflocon.flocondesktop.features.network.ui.view.filters
import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import io.github.openflocon.flocondesktop.features.network.ui.FilterUiState
import io.github.openflocon.flocondesktop.features.network.ui.NetworkAction
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkItemViewState
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkMethodUi
import io.github.openflocon.flocondesktop.features.network.ui.view.components.FilterDropdown
class MethodFilter : Filters {
override fun filter(state: FilterUiState, list: List<NetworkItemViewState>): List<NetworkItemViewState> {
if (state.methods.isEmpty())
return list
return list.filter { item ->
when (item.method) {
NetworkMethodUi.GraphQl.MUTATION -> state.methods.contains(NetworkMethodUi.GraphQl.MUTATION)
NetworkMethodUi.GraphQl.QUERY -> state.methods.contains(NetworkMethodUi.GraphQl.QUERY)
NetworkMethodUi.Grpc -> state.methods.contains(NetworkMethodUi.Grpc)
NetworkMethodUi.Http.DELETE -> state.methods.contains(NetworkMethodUi.Http.DELETE)
NetworkMethodUi.Http.GET -> state.methods.contains(NetworkMethodUi.Http.GET)
NetworkMethodUi.Http.POST -> state.methods.contains(NetworkMethodUi.Http.POST)
NetworkMethodUi.Http.PUT -> state.methods.contains(NetworkMethodUi.Http.PUT)
is NetworkMethodUi.OTHER -> state.methods.contains(NetworkMethodUi.OTHER.EMPTY)
}
}
}
}
@Composable
fun FilterMethods(
filterState: FilterUiState,
onAction: (NetworkAction) -> Unit
) {
FilterDropdown(
text = "Method",
icon = null // TODO Find better icon
) {
NetworkMethodUi.all()
.forEach { method ->
val selected = filterState.methods.contains(method)
val onClick = { onAction(NetworkAction.FilterMethod(method, !selected)) }
DropdownMenuItem(
text = { Text(text = method.label) },
leadingIcon = {},
trailingIcon = {
Checkbox(
checked = selected,
onCheckedChange = { onClick() },
interactionSource = null
)
},
onClick = onClick
)
}
}
}
private val NetworkMethodUi.label
get() = when (this) {
NetworkMethodUi.GraphQl.MUTATION -> "GraphQL - Mutation"
NetworkMethodUi.GraphQl.QUERY -> "GraphQL - Query"
NetworkMethodUi.Grpc -> "Grpc"
NetworkMethodUi.Http.DELETE -> "Http - DELETE"
NetworkMethodUi.Http.GET -> "Http - GET"
NetworkMethodUi.Http.POST -> "Http - POST"
NetworkMethodUi.Http.PUT -> "Http - PUT"
is NetworkMethodUi.OTHER -> "Other"
}

View file

@ -0,0 +1,70 @@
package io.github.openflocon.flocondesktop.features.network.ui.view.header
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.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.Delete
import androidx.compose.material3.Icon
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.unit.dp
import io.github.openflocon.flocondesktop.features.network.ui.NetworkAction
import io.github.openflocon.flocondesktop.features.network.ui.NetworkUiState
import io.github.openflocon.flocondesktop.features.network.ui.view.components.FilterBar
import io.github.openflocon.flocondesktop.features.network.ui.view.filters.FilterMethods
@Composable
fun NetworkFilter(
uiState: NetworkUiState,
onAction: (NetworkAction) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterBar(
placeholderText = "Filter route",
onTextChange = { onAction(NetworkAction.FilterQuery(it)) },
modifier = Modifier.weight(1f)
)
Box(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = { onAction(NetworkAction.Reset) })
.padding(all = 8.dp),
) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterMethods(
filterState = uiState.filterState,
onAction = onAction
)
}
}
}

View file

@ -1,77 +0,0 @@
package io.github.openflocon.flocondesktop.features.network.ui.view.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
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.unit.dp
import io.github.openflocon.flocondesktop.features.network.ui.model.NetworkItemViewState
@Composable
fun NetworkFilterBar(
networkItems: List<NetworkItemViewState>,
onItemsChange: (List<NetworkItemViewState>) -> Unit,
onResetClicked: () -> Unit,
modifier: Modifier = Modifier,
) {
var filterText by remember {
mutableStateOf("")
}
val onItemsChangeCallback by rememberUpdatedState(onItemsChange)
val filteredNetworkItems: List<NetworkItemViewState> =
remember(networkItems, filterText) {
if (filterText.isBlank()) {
networkItems
} else {
networkItems.filter {
it.contains(filterText)
}
}
}
LaunchedEffect(filteredNetworkItems) {
onItemsChangeCallback(filteredNetworkItems)
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
FilterBar(
placeholderText = "Filter Route",
modifier = Modifier.weight(1f),
onTextChange = {
filterText = it
},
)
Box(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.clickable(onClick = onResetClicked)
.padding(all = 8.dp),
) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
}
}
}

View file

@ -1,6 +1,7 @@
package io.github.openflocon.flocondesktop
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
@ -13,6 +14,7 @@ import coil3.network.ktor3.KtorNetworkFetcherFactory
import flocondesktop.composeapp.generated.resources.Res
import flocondesktop.composeapp.generated.resources.app_icon_small
import org.jetbrains.compose.resources.painterResource
import java.awt.Dimension
fun main() = application {
startKoinApp()
@ -32,6 +34,8 @@ fun main() = application {
position = WindowPosition(Alignment.Center),
),
) {
window.minimumSize = Dimension(1200, 800)
App()
}
}

View file

@ -27,6 +27,7 @@ room = "2.7.1"
ksp = "2.2.0-2.0.2"
ktlintGradle = "13.0.0"
aboutLibraries = "12.2.4"
other-molecule = "2.1.0"
kotlinStdlib = "2.2.0"
runner = "1.5.2"
core = "1.5.0"
@ -44,7 +45,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver
androidx-lifecycle-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
kotlinx-coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref= "kotlinx-coroutines" }
kotlinx-coroutinesCore = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-dateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-dateTime" }
kotlinx-serializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
@ -57,12 +58,12 @@ ktor-serverContentNegociation = { module = "io.ktor:ktor-server-content-negotiat
ktor-serverWebsocket = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
ktor-serverTestHost = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor" }
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core"}
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel"}
koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation"}
koin-android = { module = "io.insert-koin:koin-android"}
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings"}
multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatform-settings"}
koin-core = { module = "io.insert-koin:koin-core" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" }
koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation" }
koin-android = { module = "io.insert-koin:koin-android" }
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatform-settings" }
multiplatform-settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "multiplatform-settings" }
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }