diff --git a/FloconDesktop/composeApp/build.gradle.kts b/FloconDesktop/composeApp/build.gradle.kts index d3a86d33..39b72825 100644 --- a/FloconDesktop/composeApp/build.gradle.kts +++ b/FloconDesktop/composeApp/build.gradle.kts @@ -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) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/ui/ViewModelEvent.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/ui/ViewModelEvent.kt new file mode 100644 index 00000000..aa993695 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/common/ui/ViewModelEvent.kt @@ -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 { + + val events: SharedFlow + + fun ViewModel.sendEvents(vararg event: E) + + interface Event + +} + +class ViewModelEventImpl : ViewModelEvent { + + private val _events = MutableSharedFlow() + override val events: SharedFlow = _events.asSharedFlow() + + override fun ViewModel.sendEvents(vararg event: E) { + viewModelScope.launch { + event.forEach { _events.emit(it) } + } + } + +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/ContentUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/ContentUiState.kt new file mode 100644 index 00000000..29b403c0 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/ContentUiState.kt @@ -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 +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/FilterUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/FilterUiState.kt new file mode 100644 index 00000000..fc972fc5 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/FilterUiState.kt @@ -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 +) + +fun previewFilterUiState() = FilterUiState( + query = "", + methods = emptyList() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkAction.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkAction.kt new file mode 100644 index 00000000..b1625f3d --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkAction.kt @@ -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 + +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkEvent.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkEvent.kt new file mode 100644 index 00000000..8694de15 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkEvent.kt @@ -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 diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkUiState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkUiState.kt new file mode 100644 index 00000000..8346b654 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkUiState.kt @@ -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, + val contentState: ContentUiState, + val detailState: NetworkDetailViewState?, + val filterState: FilterUiState +) + +fun previewNetworkUiState() = NetworkUiState( + items = emptyList(), + detailState = null, + contentState = previewContentUiState(), + filterState = previewFilterUiState() +) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkViewModel.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkViewModel.kt index bcad8f69..f72e3c09 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkViewModel.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/NetworkViewModel.kt @@ -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> = - 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(null) + private val contentState = MutableStateFlow(ContentUiState(selectedRequestId = null)) + private val filterUiState = MutableStateFlow(FilterUiState(query = "", methods = NetworkMethodUi.all())) - val detailState: StateFlow = - clickedRequestId + private val detailState: StateFlow = + 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, + filterState: FilterUiState + ): List { + 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 + } + } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/mapper/TypeUiMapper.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/mapper/TypeUiMapper.kt index 1271e54f..9055e012 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/mapper/TypeUiMapper.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/mapper/TypeUiMapper.kt @@ -12,6 +12,7 @@ fun toTypeUi(networkRequest: FloconHttpRequestDomainModel): NetworkItemViewState val query = extractPath(networkRequest.url) NetworkItemViewState.NetworkTypeUi.Url( query = query, + method = networkRequest.request.method ) } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/NetworkItemViewState.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/NetworkItemViewState.kt index a888ba89..d6ed8b5e 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/NetworkItemViewState.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/NetworkItemViewState.kt @@ -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" ), ) diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/NetworkMethodUi.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/NetworkMethodUi.kt index 35ea1dee..8d376e6a 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/NetworkMethodUi.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/NetworkMethodUi.kt @@ -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 + ) + } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/OnNetworkItemUserAction.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/OnNetworkItemUserAction.kt deleted file mode 100644 index 3a47ddce..00000000 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/model/OnNetworkItemUserAction.kt +++ /dev/null @@ -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 -} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/NetworkItemView.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/NetworkItemView.kt index fec2953b..e89be3c5 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/NetworkItemView.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/NetworkItemView.kt @@ -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 = {} ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/NetworkScreen.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/NetworkScreen.kt index 9dd7508c..b3a59703 100644 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/NetworkScreen.kt +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/NetworkScreen.kt @@ -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, - 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>(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 = {} ) } } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/components/Filters.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/components/Filters.kt new file mode 100644 index 00000000..779e5c73 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/components/Filters.kt @@ -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 }) + } + } +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/Filters.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/Filters.kt new file mode 100644 index 00000000..6daca244 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/Filters.kt @@ -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): List + +} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/HostFilter.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/HostFilter.kt new file mode 100644 index 00000000..eaa94a15 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/HostFilter.kt @@ -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 +//) : Filters { +// override val sort: Int +// get() = 1 +// +// override val content: @Composable (() -> Unit) = { +// +// } +// +// override fun filter(list: List): Flow> { +// return flowOf(list) +// } +// +//} diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/MethodFilter.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/MethodFilter.kt new file mode 100644 index 00000000..f2455f7d --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/filters/MethodFilter.kt @@ -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): List { + 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" + } diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/header/NetworkFilter.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/header/NetworkFilter.kt new file mode 100644 index 00000000..ab60ba65 --- /dev/null +++ b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/header/NetworkFilter.kt @@ -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 + ) + } + } +} + diff --git a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/header/NetworkFilterBar.kt b/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/header/NetworkFilterBar.kt deleted file mode 100644 index dcb3b935..00000000 --- a/FloconDesktop/composeApp/src/commonMain/kotlin/io/github/openflocon/flocondesktop/features/network/ui/view/header/NetworkFilterBar.kt +++ /dev/null @@ -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, - onItemsChange: (List) -> Unit, - onResetClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - var filterText by remember { - mutableStateOf("") - } - val onItemsChangeCallback by rememberUpdatedState(onItemsChange) - val filteredNetworkItems: List = - 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), - ) - } - } -} diff --git a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/Main.kt b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/Main.kt index 27440557..78f0dbc3 100644 --- a/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/Main.kt +++ b/FloconDesktop/composeApp/src/desktopMain/kotlin/io/github/openflocon/flocondesktop/Main.kt @@ -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() } } diff --git a/FloconDesktop/gradle/libs.versions.toml b/FloconDesktop/gradle/libs.versions.toml index c9662dd5..f0a6252c 100644 --- a/FloconDesktop/gradle/libs.versions.toml +++ b/FloconDesktop/gradle/libs.versions.toml @@ -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" }