@@ -21,12 +21,10 @@ import androidx.compose.foundation.layout.height
21
21
import androidx.compose.foundation.layout.padding
22
22
import androidx.compose.foundation.layout.size
23
23
import androidx.compose.foundation.layout.statusBars
24
+ import androidx.compose.foundation.layout.width
24
25
import androidx.compose.foundation.lazy.LazyColumn
25
- import androidx.compose.foundation.rememberScrollState
26
26
import androidx.compose.foundation.shape.RoundedCornerShape
27
- import androidx.compose.foundation.verticalScroll
28
27
import androidx.compose.material.icons.Icons
29
- import androidx.compose.material.icons.filled.Clear
30
28
import androidx.compose.material.icons.filled.Search
31
29
import androidx.compose.material.icons.outlined.ArrowDropDown
32
30
import androidx.compose.material.icons.outlined.Lock
@@ -42,8 +40,6 @@ import androidx.compose.material3.ListItemDefaults
42
40
import androidx.compose.material3.MaterialTheme
43
41
import androidx.compose.material3.ModalBottomSheet
44
42
import androidx.compose.material3.Scaffold
45
- import androidx.compose.material3.SearchBar
46
- import androidx.compose.material3.SearchBarDefaults
47
43
import androidx.compose.material3.Text
48
44
import androidx.compose.runtime.Composable
49
45
import androidx.compose.runtime.LaunchedEffect
@@ -52,19 +48,14 @@ import androidx.compose.runtime.derivedStateOf
52
48
import androidx.compose.runtime.getValue
53
49
import androidx.compose.runtime.mutableStateOf
54
50
import androidx.compose.runtime.remember
55
- import androidx.compose.runtime.saveable.rememberSaveable
56
51
import androidx.compose.runtime.setValue
57
52
import androidx.compose.ui.Alignment
58
53
import androidx.compose.ui.Modifier
59
54
import androidx.compose.ui.draw.alpha
60
55
import androidx.compose.ui.draw.clip
61
- import androidx.compose.ui.focus.FocusRequester
62
- import androidx.compose.ui.focus.focusRequester
63
56
import androidx.compose.ui.focus.onFocusChanged
64
57
import androidx.compose.ui.graphics.Color
65
58
import androidx.compose.ui.platform.LocalClipboardManager
66
- import androidx.compose.ui.platform.LocalFocusManager
67
- import androidx.compose.ui.platform.LocalSoftwareKeyboardController
68
59
import androidx.compose.ui.res.painterResource
69
60
import androidx.compose.ui.res.stringResource
70
61
import androidx.compose.ui.text.SpanStyle
@@ -113,7 +104,8 @@ data class MainViewNavigation(
113
104
val onNavigateToSettings : () -> Unit ,
114
105
val onNavigateToPeerDetails : (Tailcfg .Node ) -> Unit ,
115
106
val onNavigateToExitNodes : () -> Unit ,
116
- val onNavigateToHealth : () -> Unit
107
+ val onNavigateToHealth : () -> Unit ,
108
+ val onNavigateToSearch : () -> Unit ,
117
109
)
118
110
119
111
@OptIn(ExperimentalPermissionsApi ::class , ExperimentalMaterial3Api ::class )
@@ -195,7 +187,11 @@ fun MainView(
195
187
when (user) {
196
188
null -> SettingsButton { navigation.onNavigateToSettings() }
197
189
else -> {
198
- Avatar (profile = user, size = 36 , { navigation.onNavigateToSettings() }, isFocusable= true )
190
+ Avatar (
191
+ profile = user,
192
+ size = 36 ,
193
+ { navigation.onNavigateToSettings() },
194
+ isFocusable = true )
199
195
}
200
196
}
201
197
}
@@ -220,7 +216,7 @@ fun MainView(
220
216
PeerList (
221
217
viewModel = viewModel,
222
218
onNavigateToPeerDetails = navigation.onNavigateToPeerDetails,
223
- onSearch = { viewModel.searchPeers(it) } )
219
+ onSearchBarClick = navigation.onNavigateToSearch )
224
220
}
225
221
Ipn .State .NoState ,
226
222
Ipn .State .Starting -> StartingView ()
@@ -523,24 +519,22 @@ fun ConnectView(
523
519
fun PeerList (
524
520
viewModel : MainViewModel ,
525
521
onNavigateToPeerDetails : (Tailcfg .Node ) -> Unit ,
526
- onSearch : (String ) -> Unit
522
+ onSearchBarClick : () -> Unit
527
523
) {
528
524
val peerList by viewModel.peers.collectAsState(initial = emptyList<PeerSet >())
529
525
val searchTermStr by viewModel.searchTerm.collectAsState(initial = " " )
530
526
val showNoResults =
531
527
remember { derivedStateOf { searchTermStr.isNotEmpty() && peerList.isEmpty() } }.value
532
528
533
529
val netmap = viewModel.netmap.collectAsState()
534
- val focusManager = LocalFocusManager .current
535
- var isFocussed by remember { mutableStateOf(false ) }
536
530
var isListFocussed by remember { mutableStateOf(false ) }
537
531
val expandedPeer = viewModel.expandedMenuPeer.collectAsState()
538
532
val localClipboardManager = LocalClipboardManager .current
539
533
val enableSearch = ! isAndroidTV()
540
534
541
535
Column (modifier = Modifier .fillMaxSize()) {
542
536
if (enableSearch) {
543
- SearchWithDynamicSuggestions (viewModel, onSearch )
537
+ Search (onSearchBarClick )
544
538
545
539
Spacer (modifier = Modifier .height(if (showNoResults) 0 .dp else 8 .dp))
546
540
}
@@ -701,98 +695,38 @@ fun PromptPermissionsIfNecessary() {
701
695
702
696
@OptIn(ExperimentalMaterial3Api ::class )
703
697
@Composable
704
- fun SearchWithDynamicSuggestions (viewModel : MainViewModel , onSearch : (String ) -> Unit ) {
705
- val searchTerm by viewModel.searchTerm.collectAsState()
706
- val filteredPeers by viewModel.peers.collectAsState()
707
- var expanded by rememberSaveable { mutableStateOf(false ) }
708
- val netmap by viewModel.netmap.collectAsState()
709
-
710
- val keyboardController = LocalSoftwareKeyboardController .current
711
- val focusRequester = remember { FocusRequester () }
712
- val focusManager = LocalFocusManager .current
698
+ fun Search (
699
+ onSearchBarClick : () -> Unit // Callback for navigating to SearchView
700
+ ) {
701
+ // Prevent multiple taps
702
+ var isNavigating by remember { mutableStateOf(false ) }
713
703
714
- Column (
704
+ // Outer Box to handle clicks
705
+ Box (
715
706
modifier =
716
- Modifier .fillMaxWidth().focusRequester(focusRequester).clickable {
717
- focusRequester.requestFocus()
718
- keyboardController?.show()
719
- }) {
720
- SearchBar (
721
- modifier = Modifier .fillMaxWidth().align(Alignment .CenterHorizontally ),
722
- inputField = {
723
- SearchBarDefaults .InputField (
724
- query = searchTerm,
725
- onQueryChange = { query ->
726
- viewModel.updateSearchTerm(query)
727
- onSearch(query)
728
- expanded = query.isNotEmpty()
729
- },
730
- onSearch = { query ->
731
- viewModel.updateSearchTerm(query)
732
- onSearch(query)
733
- expanded = false
734
- },
735
- expanded = expanded,
736
- onExpandedChange = { expanded = it },
737
- placeholder = { Text (" Search" ) },
738
- leadingIcon = { Icon (Icons .Default .Search , contentDescription = null ) },
739
- trailingIcon = {
740
- if (expanded) {
741
- IconButton (
742
- onClick = {
743
- viewModel.updateSearchTerm(" " )
744
- onSearch(" " )
745
- expanded = false
746
- focusManager.clearFocus()
747
- keyboardController?.hide()
748
- }) {
749
- Icon (Icons .Default .Clear , contentDescription = " Clear search" )
750
- }
751
- }
752
- })
753
- },
754
- expanded = expanded,
755
- onExpandedChange = { expanded = it },
756
- content = {
757
- // Search results or suggestions
758
- Column (Modifier .verticalScroll(rememberScrollState()).fillMaxSize()) {
759
- filteredPeers.forEach { peerSet ->
760
- val userName = peerSet.user?.DisplayName ? : " Unknown User"
761
- peerSet.peers.forEach { peer ->
762
- val deviceName = peer.displayName ? : " Unknown Device"
763
- val ipAddress = peer.Addresses ?.firstOrNull()?.split(" /" )?.first() ? : " No IP"
764
-
765
- ListItem (
766
- headlineContent = { Text (userName) },
767
- supportingContent = {
768
- Column {
769
- Row (verticalAlignment = Alignment .CenterVertically ) {
770
- val onlineColor = peer.connectedColor(netmap)
771
- Box (
772
- modifier =
773
- Modifier .size(10 .dp)
774
- .background(onlineColor, shape = RoundedCornerShape (50 )))
775
- Spacer (modifier = Modifier .size(8 .dp))
776
- Text (deviceName)
777
- }
778
- Text (ipAddress)
779
- }
780
- },
781
- colors = ListItemDefaults .colors(containerColor = Color .Transparent ),
782
- modifier =
783
- Modifier .clickable {
784
- viewModel.updateSearchTerm(userName)
785
- onSearch(userName)
786
- expanded = false
787
- focusManager.clearFocus()
788
- keyboardController?.hide()
789
- }
790
- .fillMaxWidth()
791
- .padding(horizontal = 16 .dp, vertical = 4 .dp))
792
- }
793
- }
707
+ Modifier .fillMaxWidth()
708
+ .height(56 .dp)
709
+ .clip(RoundedCornerShape (28 .dp))
710
+ .background(MaterialTheme .colorScheme.surface)
711
+ .clickable(enabled = ! isNavigating) { // Intercept taps
712
+ isNavigating = true
713
+ onSearchBarClick() // Trigger navigation
794
714
}
795
- })
715
+ .padding(horizontal = 16 .dp)) {
716
+ Row (verticalAlignment = Alignment .CenterVertically , modifier = Modifier .fillMaxSize()) {
717
+ Icon (
718
+ imageVector = Icons .Default .Search ,
719
+ contentDescription = stringResource(R .string.search),
720
+ tint = MaterialTheme .colorScheme.onSurfaceVariant,
721
+ modifier = Modifier .padding(start = 16 .dp))
722
+ Spacer (modifier = Modifier .width(8 .dp))
723
+ // Placeholder Text
724
+ Text (
725
+ text = stringResource(R .string.search_ellipsis),
726
+ style = MaterialTheme .typography.bodyMedium,
727
+ color = MaterialTheme .colorScheme.onSurfaceVariant,
728
+ modifier = Modifier .weight(1f ))
729
+ }
796
730
}
797
731
}
798
732
@@ -808,6 +742,7 @@ fun MainViewPreview() {
808
742
onNavigateToSettings = {},
809
743
onNavigateToPeerDetails = {},
810
744
onNavigateToExitNodes = {},
811
- onNavigateToHealth = {}),
745
+ onNavigateToHealth = {},
746
+ onNavigateToSearch = {}),
812
747
vm)
813
748
}
0 commit comments