Add search functionality to connections
- Add performSearch() method to Connection with support for plain text and typed search (network:, inbound:, outbound:, rule:, package:, etc.) - Add search state management in ConnectionsViewModel - Move filter/sort/search controls to header in ComposeActivity - Add collapsible search bar with AnimatedVisibility
This commit is contained in:
@@ -22,12 +22,16 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.ExpandLess
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
import androidx.compose.material.icons.filled.Folder
|
import androidx.compose.material.icons.filled.Folder
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Pause
|
import androidx.compose.material.icons.filled.Pause
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material.icons.filled.SwapVert
|
||||||
import androidx.compose.material.icons.filled.UnfoldLess
|
import androidx.compose.material.icons.filled.UnfoldLess
|
||||||
import androidx.compose.material.icons.filled.UnfoldMore
|
import androidx.compose.material.icons.filled.UnfoldMore
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -40,6 +44,7 @@ import androidx.compose.material3.AlertDialog
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||||
import androidx.compose.material3.Badge
|
import androidx.compose.material3.Badge
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -101,6 +106,8 @@ import io.nekohasekai.sfa.compose.screen.connections.ConnectionDetailsScreen
|
|||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsScreen
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsScreen
|
||||||
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
import io.nekohasekai.sfa.compose.screen.connections.ConnectionsViewModel
|
||||||
import io.nekohasekai.sfa.ui.connections.Connection
|
import io.nekohasekai.sfa.ui.connections.Connection
|
||||||
|
import io.nekohasekai.sfa.ui.connections.ConnectionSort
|
||||||
|
import io.nekohasekai.sfa.ui.connections.ConnectionStateFilter
|
||||||
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
import io.nekohasekai.sfa.compose.screen.dashboard.groups.GroupsViewModel
|
||||||
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
import io.nekohasekai.sfa.compose.screen.log.LogViewModel
|
||||||
import io.nekohasekai.sfa.compose.theme.SFATheme
|
import io.nekohasekai.sfa.compose.theme.SFATheme
|
||||||
@@ -856,22 +863,124 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
var showStateMenu by remember { mutableStateOf(false) }
|
||||||
|
var showSortMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(bottom = 16.dp),
|
.padding(bottom = 8.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.title_connections),
|
text = stringResource(R.string.title_connections),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
// State Filter
|
||||||
|
Box {
|
||||||
|
FilterChip(
|
||||||
|
selected = connectionsUiState.stateFilter != ConnectionStateFilter.Active,
|
||||||
|
onClick = { showStateMenu = true },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
when (connectionsUiState.stateFilter) {
|
||||||
|
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
||||||
|
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
||||||
|
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.FilterList, contentDescription = null)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showStateMenu,
|
||||||
|
onDismissRequest = { showStateMenu = false },
|
||||||
|
) {
|
||||||
|
ConnectionStateFilter.entries.forEach { filter ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
when (filter) {
|
||||||
|
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
||||||
|
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
||||||
|
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
connectionsViewModel.setStateFilter(filter)
|
||||||
|
showStateMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (connectionsUiState.stateFilter == filter) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
Box {
|
||||||
|
FilterChip(
|
||||||
|
selected = connectionsUiState.sort != ConnectionSort.ByDate,
|
||||||
|
onClick = { showSortMenu = true },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
when (connectionsUiState.sort) {
|
||||||
|
ConnectionSort.ByDate -> stringResource(R.string.connection_sort_date)
|
||||||
|
ConnectionSort.ByTraffic -> stringResource(R.string.connection_sort_traffic)
|
||||||
|
ConnectionSort.ByTrafficTotal -> stringResource(R.string.connection_sort_traffic_total)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.SwapVert, contentDescription = null)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showSortMenu,
|
||||||
|
onDismissRequest = { showSortMenu = false },
|
||||||
|
) {
|
||||||
|
ConnectionSort.entries.forEach { sort ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
when (sort) {
|
||||||
|
ConnectionSort.ByDate -> stringResource(R.string.connection_sort_date)
|
||||||
|
ConnectionSort.ByTraffic -> stringResource(R.string.connection_sort_traffic)
|
||||||
|
ConnectionSort.ByTrafficTotal -> stringResource(R.string.connection_sort_traffic_total)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
connectionsViewModel.setSort(sort)
|
||||||
|
showSortMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
if (connectionsUiState.sort == sort) {
|
||||||
|
Icon(Icons.Default.Check, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = { showConnectionsMenu = true }) {
|
IconButton(onClick = { showConnectionsMenu = true }) {
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||||
@@ -881,12 +990,36 @@ class ComposeActivity : ComponentActivity(), ServiceConnection.Callback {
|
|||||||
expanded = showConnectionsMenu,
|
expanded = showConnectionsMenu,
|
||||||
onDismissRequest = { showConnectionsMenu = false },
|
onDismissRequest = { showConnectionsMenu = false },
|
||||||
) {
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
if (connectionsUiState.isSearchActive) {
|
||||||
|
stringResource(R.string.close_search)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.search)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
connectionsViewModel.toggleSearch()
|
||||||
|
showConnectionsMenu = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (connectionsUiState.isSearchActive) Icons.Default.Close else Icons.Default.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.connection_close_all)) },
|
text = { Text(stringResource(R.string.connection_close_all)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
connectionsViewModel.closeAllConnections()
|
connectionsViewModel.closeAllConnections()
|
||||||
showConnectionsMenu = false
|
showConnectionsMenu = false
|
||||||
},
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = null)
|
||||||
|
},
|
||||||
enabled = connectionsUiState.connections.any { it.isActive },
|
enabled = connectionsUiState.connections.any { it.isActive },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package io.nekohasekai.sfa.compose.screen.connections
|
package io.nekohasekai.sfa.compose.screen.connections
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -18,33 +22,29 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Clear
|
||||||
import androidx.compose.material.icons.filled.FilterList
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.SwapVert
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import io.nekohasekai.sfa.R
|
import io.nekohasekai.sfa.R
|
||||||
import io.nekohasekai.sfa.constant.Status
|
import io.nekohasekai.sfa.constant.Status
|
||||||
import io.nekohasekai.sfa.ui.connections.Connection
|
import io.nekohasekai.sfa.ui.connections.Connection
|
||||||
import io.nekohasekai.sfa.ui.connections.ConnectionSort
|
|
||||||
import io.nekohasekai.sfa.ui.connections.ConnectionStateFilter
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConnectionsScreen(
|
fun ConnectionsScreen(
|
||||||
@@ -54,114 +54,42 @@ fun ConnectionsScreen(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
var showStateMenu by remember { mutableStateOf(false) }
|
|
||||||
var showSortMenu by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(serviceStatus) {
|
LaunchedEffect(serviceStatus) {
|
||||||
viewModel.updateServiceStatus(serviceStatus)
|
viewModel.updateServiceStatus(serviceStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxSize()) {
|
Column(modifier = modifier.fillMaxSize()) {
|
||||||
Row(
|
AnimatedVisibility(
|
||||||
modifier = Modifier
|
visible = uiState.isSearchActive,
|
||||||
.fillMaxWidth()
|
enter = expandVertically() + fadeIn(),
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
exit = shrinkVertically() + fadeOut(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
Box {
|
val focusRequester = remember { FocusRequester() }
|
||||||
FilterChip(
|
|
||||||
selected = uiState.stateFilter != ConnectionStateFilter.All,
|
|
||||||
onClick = { showStateMenu = true },
|
|
||||||
label = {
|
|
||||||
Text(
|
|
||||||
when (uiState.stateFilter) {
|
|
||||||
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
|
||||||
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
|
||||||
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(Icons.Default.FilterList, contentDescription = null)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
LaunchedEffect(Unit) {
|
||||||
expanded = showStateMenu,
|
focusRequester.requestFocus()
|
||||||
onDismissRequest = { showStateMenu = false },
|
|
||||||
) {
|
|
||||||
ConnectionStateFilter.entries.forEach { filter ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
when (filter) {
|
|
||||||
ConnectionStateFilter.All -> stringResource(R.string.connection_state_all)
|
|
||||||
ConnectionStateFilter.Active -> stringResource(R.string.connection_state_active)
|
|
||||||
ConnectionStateFilter.Closed -> stringResource(R.string.connection_state_closed)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
viewModel.setStateFilter(filter)
|
|
||||||
showStateMenu = false
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
if (uiState.stateFilter == filter) {
|
|
||||||
Icon(Icons.Default.Check, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box {
|
OutlinedTextField(
|
||||||
FilterChip(
|
value = uiState.searchText,
|
||||||
selected = uiState.sort != ConnectionSort.ByDate,
|
onValueChange = { viewModel.setSearchText(it) },
|
||||||
onClick = { showSortMenu = true },
|
modifier = Modifier
|
||||||
label = {
|
.fillMaxWidth()
|
||||||
Text(
|
.padding(horizontal = 16.dp)
|
||||||
when (uiState.sort) {
|
.padding(bottom = 8.dp)
|
||||||
ConnectionSort.ByDate -> stringResource(R.string.connection_sort_date)
|
.focusRequester(focusRequester),
|
||||||
ConnectionSort.ByTraffic -> stringResource(R.string.connection_sort_traffic)
|
placeholder = { Text(stringResource(R.string.search_connections)) },
|
||||||
ConnectionSort.ByTrafficTotal -> stringResource(R.string.connection_sort_traffic_total)
|
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||||
}
|
trailingIcon = {
|
||||||
)
|
if (uiState.searchText.isNotEmpty()) {
|
||||||
},
|
IconButton(onClick = { viewModel.setSearchText("") }) {
|
||||||
leadingIcon = {
|
Icon(Icons.Default.Clear, contentDescription = null)
|
||||||
Icon(Icons.Default.SwapVert, contentDescription = null)
|
}
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showSortMenu,
|
|
||||||
onDismissRequest = { showSortMenu = false },
|
|
||||||
) {
|
|
||||||
ConnectionSort.entries.forEach { sort ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
when (sort) {
|
|
||||||
ConnectionSort.ByDate -> stringResource(R.string.connection_sort_date)
|
|
||||||
ConnectionSort.ByTraffic -> stringResource(R.string.connection_sort_traffic)
|
|
||||||
ConnectionSort.ByTrafficTotal -> stringResource(R.string.connection_sort_traffic_total)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
viewModel.setSort(sort)
|
|
||||||
showSortMenu = false
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
if (uiState.sort == sort) {
|
|
||||||
Icon(Icons.Default.Check, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
singleLine = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ data class ConnectionsUiState(
|
|||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val stateFilter: ConnectionStateFilter = ConnectionStateFilter.Active,
|
val stateFilter: ConnectionStateFilter = ConnectionStateFilter.Active,
|
||||||
val sort: ConnectionSort = ConnectionSort.ByDate,
|
val sort: ConnectionSort = ConnectionSort.ByDate,
|
||||||
|
val searchText: String = "",
|
||||||
|
val isSearchActive: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class ConnectionsEvent : ScreenEvent {
|
sealed class ConnectionsEvent : ScreenEvent {
|
||||||
@@ -122,6 +124,24 @@ class ConnectionsViewModel(
|
|||||||
rawConnections?.let { processConnections(it) }
|
rawConnections?.let { processConnections(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSearchText(text: String) {
|
||||||
|
updateState { copy(searchText = text) }
|
||||||
|
rawConnections?.let { processConnections(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSearch() {
|
||||||
|
val newSearchActive = !currentState.isSearchActive
|
||||||
|
updateState {
|
||||||
|
copy(
|
||||||
|
isSearchActive = newSearchActive,
|
||||||
|
searchText = if (newSearchActive) searchText else "",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!newSearchActive) {
|
||||||
|
rawConnections?.let { processConnections(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun closeConnection(connectionId: String) {
|
fun closeConnection(connectionId: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -188,6 +208,7 @@ class ConnectionsViewModel(
|
|||||||
val connectionList = connections.iterator().toList()
|
val connectionList = connections.iterator().toList()
|
||||||
.filter { it.outboundType != "dns" }
|
.filter { it.outboundType != "dns" }
|
||||||
.map { Connection.from(it) }
|
.map { Connection.from(it) }
|
||||||
|
.filter { it.performSearch(currentState.searchText) }
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
updateState {
|
updateState {
|
||||||
|
|||||||
@@ -55,6 +55,45 @@ data class Connection(
|
|||||||
) {
|
) {
|
||||||
val isActive: Boolean get() = closedAt == null || closedAt == 0L
|
val isActive: Boolean get() = closedAt == null || closedAt == 0L
|
||||||
|
|
||||||
|
fun performSearch(content: String): Boolean {
|
||||||
|
if (content.isBlank()) return true
|
||||||
|
for (item in content.trim().split(" ").filter { it.isNotEmpty() }) {
|
||||||
|
val parts = item.split(":", limit = 2)
|
||||||
|
if (parts.size == 2) {
|
||||||
|
if (!performSearchType(parts[0], parts[1])) return false
|
||||||
|
} else {
|
||||||
|
if (!performSearchPlain(item)) return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performSearchPlain(content: String): Boolean {
|
||||||
|
return destination.contains(content, ignoreCase = true) ||
|
||||||
|
domain.contains(content, ignoreCase = true) ||
|
||||||
|
outbound.contains(content, ignoreCase = true) ||
|
||||||
|
rule.contains(content, ignoreCase = true) ||
|
||||||
|
processInfo?.packageName?.contains(content, ignoreCase = true) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performSearchType(type: String, value: String): Boolean {
|
||||||
|
return when (type) {
|
||||||
|
"network" -> network.equals(value, ignoreCase = true)
|
||||||
|
"inbound" -> inbound.contains(value, ignoreCase = true)
|
||||||
|
"inbound.type" -> inboundType.equals(value, ignoreCase = true)
|
||||||
|
"source" -> source.contains(value, ignoreCase = true)
|
||||||
|
"destination" -> destination.contains(value, ignoreCase = true)
|
||||||
|
"outbound" -> outbound.contains(value, ignoreCase = true)
|
||||||
|
"outbound.type" -> outboundType.equals(value, ignoreCase = true)
|
||||||
|
"rule" -> rule.contains(value, ignoreCase = true)
|
||||||
|
"protocol" -> protocolName.equals(value, ignoreCase = true)
|
||||||
|
"user" -> user.contains(value, ignoreCase = true)
|
||||||
|
"package" -> processInfo?.packageName?.contains(value, ignoreCase = true) == true
|
||||||
|
"chain" -> chain.any { it.contains(value, ignoreCase = true) }
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun from(connection: LibboxConnection): Connection {
|
fun from(connection: LibboxConnection): Connection {
|
||||||
return Connection(
|
return Connection(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<string name="connection_sort_date">Date</string>
|
<string name="connection_sort_date">Date</string>
|
||||||
<string name="connection_sort_traffic">Traffic</string>
|
<string name="connection_sort_traffic">Traffic</string>
|
||||||
<string name="connection_sort_traffic_total">Total Traffic</string>
|
<string name="connection_sort_traffic_total">Total Traffic</string>
|
||||||
|
<string name="search_connections">Search connections…</string>
|
||||||
<string name="connection_close">Close</string>
|
<string name="connection_close">Close</string>
|
||||||
<string name="connection_close_all">Close All</string>
|
<string name="connection_close_all">Close All</string>
|
||||||
<string name="connection_state">State</string>
|
<string name="connection_state">State</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user