Skip to content

Commit 20ba7ff

Browse files
committed
feat(PhotoSearchScreen): Implement recent search list management
Note: The recent search list is currently managed at the screen level and does not utilize local storage solutions. Therefore, the list of recent searches will be cleared when the PhotoSearchScreen is closed.
1 parent 465a1f1 commit 20ba7ff

File tree

11 files changed

+322
-41
lines changed

11 files changed

+322
-41
lines changed

app/src/main/java/com/wei/picquest/navigation/PqNavHost.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import androidx.window.layout.DisplayFeature
77
import com.wei.picquest.core.designsystem.ui.DeviceOrientation
88
import com.wei.picquest.feature.home.home.navigation.homeGraph
99
import com.wei.picquest.feature.home.home.navigation.homeRoute
10-
import com.wei.picquest.feature.photo.photolibrary.navigation.navigateToPhotoLibrary
1110
import com.wei.picquest.feature.photo.photolibrary.navigation.photoLibraryGraph
1211
import com.wei.picquest.feature.photo.photosearch.navigation.photoSearchGraph
1312
import com.wei.picquest.ui.PqAppState
@@ -41,7 +40,6 @@ fun PqNavHost(
4140
)
4241
photoSearchGraph(
4342
navController = navController,
44-
onSearchClick = navController::navigateToPhotoLibrary,
4543
nestedGraphs = {
4644
photoLibraryGraph(navController = navController)
4745
},

feature/photo/src/main/java/com/wei/picquest/feature/photo/photolibrary/PhotoLibraryScreen.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@ fun BackButton(
145145
modifier = Modifier
146146
.clip(CircleShape)
147147
.background(MaterialTheme.colorScheme.surfaceVariant)
148-
.semantics { contentDescription = "Back" },
148+
.semantics { contentDescription = "Search" },
149149
) {
150150
Icon(
151-
imageVector = PqIcons.ArrowBack,
151+
imageVector = PqIcons.Search,
152152
contentDescription = null,
153153
tint = MaterialTheme.colorScheme.primary,
154154
)
@@ -281,9 +281,7 @@ fun ImageDetailItem(
281281
modifier = if (layoutType == LayoutType.LIST) {
282282
Modifier.size(300.dp)
283283
} else {
284-
Modifier.size(
285-
80.dp,
286-
)
284+
Modifier.size(120.dp)
287285
},
288286
)
289287
}

feature/photo/src/main/java/com/wei/picquest/feature/photo/photolibrary/PhotoLibraryViewModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ class PhotoLibraryViewModel @Inject constructor(
2525

2626
private val photoLibraryArgs: PhotoLibraryArgs = PhotoLibraryArgs(savedStateHandle)
2727

28-
val photoSearchKeyword = photoLibraryArgs.photoSearchKeyword
28+
val photoSearchQuery = photoLibraryArgs.photoSearchQuery
2929

3030
private val _imagesState: MutableStateFlow<PagingData<ImageDetail>> =
3131
MutableStateFlow(value = PagingData.empty())
3232
val imagesState: MutableStateFlow<PagingData<ImageDetail>> get() = _imagesState
3333

3434
init {
35-
searchImages(photoSearchKeyword)
35+
searchImages(photoSearchQuery)
3636
}
3737

3838
private fun searchImages(query: String) {

feature/photo/src/main/java/com/wei/picquest/feature/photo/photolibrary/navigation/PhotoLibraryNavigation.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,23 @@ import java.net.URLEncoder
1515
private val URL_CHARACTER_ENCODING = Charsets.UTF_8.name()
1616

1717
@VisibleForTesting
18-
internal const val photoSearchKeywordArg = "photoSearchKeyword"
18+
internal const val photoSearchQueryArg = "photoSearchQuery"
1919

20-
internal class PhotoLibraryArgs(val photoSearchKeyword: String) {
20+
internal class PhotoLibraryArgs(val photoSearchQuery: String) {
2121
constructor(savedStateHandle: SavedStateHandle) :
2222
this(
2323
URLDecoder.decode(
24-
checkNotNull(savedStateHandle[photoSearchKeywordArg]),
24+
checkNotNull(savedStateHandle[photoSearchQueryArg]),
2525
URL_CHARACTER_ENCODING,
2626
),
2727
)
2828
}
2929

30-
fun NavController.navigateToPhotoLibrary(photoSearchKeyword: String) {
31-
val encodedKey = URLEncoder.encode(photoSearchKeyword, URL_CHARACTER_ENCODING)
30+
fun NavController.navigateToPhotoLibrary(photoSearchQuery: String) {
31+
val encodedKey = URLEncoder.encode(
32+
photoSearchQuery.ifBlank { "$photoSearchQuery " },
33+
URL_CHARACTER_ENCODING,
34+
)
3235
this.navigate("$photoSearchRoute/$encodedKey") {
3336
launchSingleTop = true
3437
}
@@ -38,9 +41,9 @@ fun NavGraphBuilder.photoLibraryGraph(
3841
navController: NavController,
3942
) {
4043
composable(
41-
route = "$photoSearchRoute/{$photoSearchKeywordArg}",
44+
route = "$photoSearchRoute/{$photoSearchQueryArg}",
4245
arguments = listOf(
43-
navArgument(photoSearchKeywordArg) { type = NavType.StringType },
46+
navArgument(photoSearchQueryArg) { type = NavType.StringType },
4447
),
4548
) {
4649
PhotoLibraryRoute(

feature/photo/src/main/java/com/wei/picquest/feature/photo/photosearch/PhotoSearchScreen.kt

Lines changed: 223 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,63 @@
11
package com.wei.picquest.feature.photo.photosearch
22

3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.Arrangement
35
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Row
47
import androidx.compose.foundation.layout.Spacer
58
import androidx.compose.foundation.layout.WindowInsets
69
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.padding
712
import androidx.compose.foundation.layout.safeDrawing
813
import androidx.compose.foundation.layout.windowInsetsBottomHeight
914
import androidx.compose.foundation.layout.windowInsetsTopHeight
10-
import androidx.compose.material3.Button
15+
import androidx.compose.foundation.lazy.LazyColumn
16+
import androidx.compose.foundation.lazy.items
17+
import androidx.compose.foundation.shape.RoundedCornerShape
18+
import androidx.compose.foundation.text.KeyboardActions
19+
import androidx.compose.foundation.text.KeyboardOptions
20+
import androidx.compose.material3.Icon
21+
import androidx.compose.material3.IconButton
1122
import androidx.compose.material3.MaterialTheme
1223
import androidx.compose.material3.Surface
1324
import androidx.compose.material3.Text
25+
import androidx.compose.material3.TextField
26+
import androidx.compose.material3.TextFieldDefaults
1427
import androidx.compose.runtime.Composable
28+
import androidx.compose.runtime.LaunchedEffect
29+
import androidx.compose.runtime.getValue
1530
import androidx.compose.runtime.mutableStateOf
1631
import androidx.compose.runtime.remember
1732
import androidx.compose.ui.Alignment
33+
import androidx.compose.ui.ExperimentalComposeUiApi
1834
import androidx.compose.ui.Modifier
19-
import androidx.compose.ui.semantics.contentDescription
20-
import androidx.compose.ui.semantics.semantics
35+
import androidx.compose.ui.focus.FocusRequester
36+
import androidx.compose.ui.focus.focusRequester
37+
import androidx.compose.ui.graphics.Color
38+
import androidx.compose.ui.input.key.Key
39+
import androidx.compose.ui.input.key.key
40+
import androidx.compose.ui.input.key.onKeyEvent
41+
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
42+
import androidx.compose.ui.platform.testTag
43+
import androidx.compose.ui.res.stringResource
44+
import androidx.compose.ui.text.SpanStyle
45+
import androidx.compose.ui.text.buildAnnotatedString
46+
import androidx.compose.ui.text.font.FontWeight
47+
import androidx.compose.ui.text.input.ImeAction
48+
import androidx.compose.ui.text.withStyle
49+
import androidx.compose.ui.unit.dp
50+
import androidx.hilt.navigation.compose.hiltViewModel
51+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2152
import androidx.navigation.NavController
2253
import com.wei.picquest.core.designsystem.component.FunctionalityNotAvailablePopup
2354
import com.wei.picquest.core.designsystem.component.ThemePreviews
55+
import com.wei.picquest.core.designsystem.icon.PqIcons
2456
import com.wei.picquest.core.designsystem.theme.PqTheme
57+
import com.wei.picquest.core.designsystem.theme.SPACING_LARGE
58+
import com.wei.picquest.core.designsystem.theme.SPACING_SMALL
59+
import com.wei.picquest.feature.photo.R
60+
import com.wei.picquest.feature.photo.photolibrary.navigation.navigateToPhotoLibrary
2561

2662
/**
2763
*
@@ -54,15 +90,38 @@ import com.wei.picquest.core.designsystem.theme.PqTheme
5490
*/
5591
@Composable
5692
internal fun PhotoSearchRoute(
57-
onSearchClick: (String) -> Unit,
5893
navController: NavController,
94+
viewModel: PhotoSearchViewModel = hiltViewModel(),
5995
) {
60-
PhotoSearchScreen(onSearchClick = onSearchClick)
96+
val uiStates: PhotoSearchViewState by viewModel.states.collectAsStateWithLifecycle()
97+
98+
PhotoSearchScreen(
99+
uiStates = uiStates,
100+
onSearchQueryChanged = {
101+
viewModel.dispatch(PhotoSearchViewAction.SearchQueryChanged(it))
102+
},
103+
104+
onSearchTriggered = {
105+
viewModel.dispatch(PhotoSearchViewAction.SearchTriggered(it))
106+
navController.navigateToPhotoLibrary(it)
107+
},
108+
onRecentSearchClicked = {
109+
viewModel.dispatch(PhotoSearchViewAction.RecentSearchClicked(it))
110+
navController.navigateToPhotoLibrary(it)
111+
},
112+
onClearRecentSearches = {
113+
viewModel.dispatch(PhotoSearchViewAction.ClearRecentSearchQueriesClicked)
114+
},
115+
)
61116
}
62117

63118
@Composable
64119
internal fun PhotoSearchScreen(
65-
onSearchClick: (String) -> Unit,
120+
uiStates: PhotoSearchViewState,
121+
onSearchQueryChanged: (String) -> Unit,
122+
onSearchTriggered: (String) -> Unit,
123+
onRecentSearchClicked: (String) -> Unit,
124+
onClearRecentSearches: () -> Unit,
66125
withTopSpacer: Boolean = true,
67126
withBottomSpacer: Boolean = true,
68127
) {
@@ -88,19 +147,17 @@ internal fun PhotoSearchScreen(
88147
}
89148

90149
Column {
91-
Spacer(modifier = Modifier.weight(1f))
92-
Text(
93-
text = "Search Screen not available \uD83D\uDE48",
94-
color = MaterialTheme.colorScheme.error,
95-
style = MaterialTheme.typography.headlineMedium,
96-
modifier = Modifier
97-
.semantics { contentDescription = "" },
150+
SearchTextField(
151+
onSearchQueryChanged = onSearchQueryChanged,
152+
onSearchTriggered = onSearchTriggered,
153+
searchQuery = uiStates.searchQuery,
154+
)
155+
RecentSearchesBody(
156+
modifier = Modifier.weight(1f),
157+
onClearRecentSearches = onClearRecentSearches,
158+
onRecentSearchClicked = onRecentSearchClicked,
159+
recentSearchQueries = uiStates.recentSearchQueries,
98160
)
99-
// TODO Wei
100-
Button(onClick = { onSearchClick("kitten") }) {
101-
Text(text = "Search [kitten] keyword")
102-
}
103-
Spacer(modifier = Modifier.weight(1f))
104161
}
105162

106163
if (withBottomSpacer) {
@@ -110,10 +167,157 @@ internal fun PhotoSearchScreen(
110167
}
111168
}
112169

170+
@Composable
171+
private fun RecentSearchesBody(
172+
modifier: Modifier = Modifier,
173+
onClearRecentSearches: () -> Unit,
174+
onRecentSearchClicked: (String) -> Unit,
175+
recentSearchQueries: List<String>,
176+
) {
177+
Column(modifier = modifier) {
178+
Row(
179+
horizontalArrangement = Arrangement.SpaceBetween,
180+
verticalAlignment = Alignment.CenterVertically,
181+
modifier = Modifier.fillMaxWidth(),
182+
) {
183+
Text(
184+
text = buildAnnotatedString {
185+
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
186+
append(stringResource(id = R.string.recent_searches))
187+
}
188+
},
189+
modifier = Modifier.padding(
190+
horizontal = SPACING_LARGE.dp,
191+
vertical = SPACING_SMALL.dp,
192+
),
193+
)
194+
if (recentSearchQueries.isNotEmpty()) {
195+
IconButton(
196+
onClick = {
197+
onClearRecentSearches()
198+
},
199+
modifier = Modifier.padding(horizontal = SPACING_LARGE.dp),
200+
) {
201+
Icon(
202+
imageVector = PqIcons.Close,
203+
contentDescription = stringResource(
204+
id = R.string.clear_recent_searches_content_desc,
205+
),
206+
tint = MaterialTheme.colorScheme.onSurface,
207+
)
208+
}
209+
}
210+
}
211+
LazyColumn(modifier = Modifier.padding(horizontal = SPACING_LARGE.dp)) {
212+
items(recentSearchQueries) { recentSearch ->
213+
Text(
214+
text = recentSearch,
215+
style = MaterialTheme.typography.headlineSmall,
216+
modifier = Modifier
217+
.padding(vertical = SPACING_LARGE.dp)
218+
.clickable { onRecentSearchClicked(recentSearch) }
219+
.fillMaxWidth(),
220+
)
221+
}
222+
}
223+
}
224+
}
225+
226+
@OptIn(ExperimentalComposeUiApi::class)
227+
@Composable
228+
private fun SearchTextField(
229+
onSearchQueryChanged: (String) -> Unit,
230+
searchQuery: String,
231+
onSearchTriggered: (String) -> Unit,
232+
) {
233+
val focusRequester = remember { FocusRequester() }
234+
val keyboardController = LocalSoftwareKeyboardController.current
235+
236+
val onSearchExplicitlyTriggered = {
237+
keyboardController?.hide()
238+
onSearchTriggered(searchQuery)
239+
}
240+
241+
TextField(
242+
colors = TextFieldDefaults.colors(
243+
focusedIndicatorColor = Color.Transparent,
244+
unfocusedIndicatorColor = Color.Transparent,
245+
disabledIndicatorColor = Color.Transparent,
246+
),
247+
leadingIcon = {
248+
Icon(
249+
imageVector = PqIcons.Search,
250+
contentDescription = stringResource(R.string.search),
251+
tint = MaterialTheme.colorScheme.onSurface,
252+
)
253+
},
254+
trailingIcon = {
255+
if (searchQuery.isNotEmpty()) {
256+
IconButton(
257+
onClick = {
258+
onSearchQueryChanged("")
259+
},
260+
) {
261+
Icon(
262+
imageVector = PqIcons.Close,
263+
contentDescription = stringResource(R.string.clear_search_text_content_desc),
264+
tint = MaterialTheme.colorScheme.onSurface,
265+
)
266+
}
267+
}
268+
},
269+
onValueChange = {
270+
if (!it.contains("\n")) {
271+
onSearchQueryChanged(it)
272+
}
273+
},
274+
modifier = Modifier
275+
.fillMaxWidth()
276+
.padding(SPACING_LARGE.dp)
277+
.focusRequester(focusRequester)
278+
.onKeyEvent {
279+
if (it.key == Key.Enter) {
280+
onSearchExplicitlyTriggered()
281+
true
282+
} else {
283+
false
284+
}
285+
}
286+
.testTag("searchTextField"),
287+
shape = RoundedCornerShape(32.dp),
288+
value = searchQuery,
289+
keyboardOptions = KeyboardOptions(
290+
imeAction = ImeAction.Search,
291+
),
292+
keyboardActions = KeyboardActions(
293+
onSearch = {
294+
onSearchExplicitlyTriggered()
295+
},
296+
),
297+
maxLines = 1,
298+
singleLine = true,
299+
)
300+
LaunchedEffect(Unit) {
301+
focusRequester.requestFocus()
302+
}
303+
}
304+
113305
@ThemePreviews
114306
@Composable
115307
fun SearchPhotoScreenPreview() {
116308
PqTheme {
117-
PhotoSearchScreen(onSearchClick = {})
309+
PhotoSearchScreen(
310+
uiStates = PhotoSearchViewState(
311+
searchQuery = "cat",
312+
recentSearchQueries = listOf(
313+
"cat",
314+
"mouse",
315+
),
316+
),
317+
onSearchQueryChanged = {},
318+
onSearchTriggered = {},
319+
onRecentSearchClicked = {},
320+
onClearRecentSearches = {},
321+
)
118322
}
119323
}

0 commit comments

Comments
 (0)