Skip to content

Commit ff59b28

Browse files
authored
Merge pull request #69 from psuzn/develop
* add search bar (#67) * General improvements (#68) * nav animation improvements * General improvements
2 parents 2639b82 + a110626 commit ff59b28

File tree

16 files changed

+232
-55
lines changed

16 files changed

+232
-55
lines changed

desktopApp/src/jvmMain/kotlin/Main.kt

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import androidx.compose.ui.unit.DpSize
32
import androidx.compose.ui.unit.dp
43
import androidx.compose.ui.window.WindowState

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/navigation/NavHost.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fun NavHost(navGraph: NavGraph) {
3434
currentEntry?.also { entry ->
3535
AnimatedContent(entry, transitionSpec = navigator.transitionSpec) {
3636
savableStateHolder.SaveableStateProvider(it.id) {
37-
entry.destination.content()
37+
it.destination.content()
3838
}
3939
}
4040
}

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/AppStrings.kt

+1
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ interface AppStrings {
5454
val permissionRequired: String
5555
val grantPermission: String
5656
val displayCurrencyDescription: String
57+
val search: String
5758
}

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/strings/en.kt

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ object StringEn : AppStrings {
2121
override val moreInfo = "More Info"
2222
override val aboutMe = "About me"
2323
override val whatsNew = "What's New"
24+
override val search = "Search"
2425

2526
// Settings Screen
2627
override val appearance = "Appearance"

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/common/Scaffold.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import me.sujanpoudel.playdeals.common.navigation.Navigator
1313
@Composable
1414
fun Scaffold(
1515
modifier: Modifier = Modifier,
16-
title: String? = null,
17-
showNavBackIcon: Boolean = true,
16+
title: ScaffoldToolbar.ScaffoldTitle = ScaffoldToolbar.ScaffoldTitle.None,
17+
showNavIcon: Boolean = true,
1818
navigationIcon: @Composable (Navigator) -> Unit = { it -> ScaffoldToolbar.NavigationIcon(it) },
1919
actions: (@Composable (Navigator) -> Unit)? = null,
2020
content: @Composable BoxScope.() -> Unit,
@@ -23,7 +23,7 @@ fun Scaffold(
2323
topBar = {
2424
ScaffoldToolbar(
2525
title = title,
26-
showNavBackIcon = showNavBackIcon,
26+
showNavIcon = showNavIcon,
2727
actions = actions,
2828
navigationIcon = navigationIcon,
2929
)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
package me.sujanpoudel.playdeals.common.ui.components.common
22

3+
import androidx.compose.animation.AnimatedContent
4+
import androidx.compose.animation.core.tween
5+
import androidx.compose.animation.fadeIn
6+
import androidx.compose.animation.fadeOut
7+
import androidx.compose.animation.togetherWith
8+
import androidx.compose.foundation.interaction.MutableInteractionSource
9+
import androidx.compose.foundation.layout.WindowInsets
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.height
12+
import androidx.compose.foundation.layout.heightIn
13+
import androidx.compose.foundation.layout.windowInsetsPadding
14+
import androidx.compose.foundation.text.BasicTextField
15+
import androidx.compose.material.ExperimentalMaterialApi
16+
import androidx.compose.material.TextFieldDefaults
317
import androidx.compose.material.icons.Icons
418
import androidx.compose.material.icons.filled.ArrowBack
519
import androidx.compose.material3.CenterAlignedTopAppBar
@@ -12,54 +26,153 @@ import androidx.compose.material3.TopAppBarDefaults
1226
import androidx.compose.material3.TopAppBarScrollBehavior
1327
import androidx.compose.material3.rememberTopAppBarState
1428
import androidx.compose.runtime.Composable
29+
import androidx.compose.runtime.Immutable
30+
import androidx.compose.runtime.LaunchedEffect
31+
import androidx.compose.runtime.State
32+
import androidx.compose.runtime.remember
1533
import androidx.compose.ui.Modifier
34+
import androidx.compose.ui.focus.FocusRequester
35+
import androidx.compose.ui.focus.focusRequester
1636
import androidx.compose.ui.graphics.Color
37+
import androidx.compose.ui.graphics.SolidColor
1738
import androidx.compose.ui.text.font.FontWeight
39+
import androidx.compose.ui.text.input.VisualTransformation
1840
import androidx.compose.ui.text.style.TextAlign
41+
import androidx.compose.ui.unit.dp
42+
import androidx.compose.ui.unit.sp
1943
import me.sujanpoudel.playdeals.common.navigation.Navigator
44+
import me.sujanpoudel.playdeals.common.strings.Strings
45+
46+
@Composable
47+
fun rememberTextTitle(title: String): ScaffoldToolbar.ScaffoldTitle {
48+
return remember(title) {
49+
ScaffoldToolbar.ScaffoldTitle.TextTitle(title)
50+
}
51+
}
2052

2153
object ScaffoldToolbar {
2254

55+
sealed class ScaffoldTitle {
56+
data object None : ScaffoldTitle()
57+
58+
@Immutable
59+
data class TextTitle(val text: String) : ScaffoldTitle()
60+
61+
@Immutable
62+
class SearchBarTitle(val text: State<String>, val onTextUpdated: (String) -> Unit) : ScaffoldTitle()
63+
}
64+
2365
@Composable
2466
fun NavigationIcon(navigator: Navigator) {
2567
IconButton(onClick = navigator::pop) {
2668
Icon(Icons.Default.ArrowBack, contentDescription = "")
2769
}
2870
}
2971

30-
@OptIn(ExperimentalMaterial3Api::class)
72+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
3173
@Composable
3274
operator fun invoke(
3375
modifier: Modifier = Modifier,
34-
title: String? = null,
35-
showNavBackIcon: Boolean = true,
76+
title: ScaffoldTitle,
77+
showNavIcon: Boolean = true,
78+
alwaysShowNavIcon: Boolean = false,
3679
navigationIcon: @Composable (Navigator) -> Unit = { it -> NavigationIcon(it) },
3780
actions: (@Composable (Navigator) -> Unit)? = null,
3881
behaviour: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()),
3982
) {
4083
val navigator = Navigator.current
41-
4284
CenterAlignedTopAppBar(
43-
modifier = modifier,
44-
title = {
45-
title?.let {
46-
Text(
47-
text = title,
48-
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
49-
textAlign = TextAlign.Center,
50-
)
51-
}
52-
},
53-
navigationIcon = {
54-
if (showNavBackIcon && navigator.backStackCount.value > 1) {
55-
navigationIcon(navigator)
56-
}
57-
},
58-
actions = { actions?.invoke(navigator) },
85+
modifier = modifier.windowInsetsPadding(WindowInsets(top = 10.dp)),
86+
title = { ToolbarTitle(title) },
87+
navigationIcon =
88+
{
89+
if (alwaysShowNavIcon || (showNavIcon && navigator.backStackCount.value > 1)) {
90+
navigationIcon(navigator)
91+
}
92+
},
93+
actions =
94+
{ actions?.invoke(navigator) },
5995
colors = TopAppBarDefaults.topAppBarColors(
6096
containerColor = Color.Transparent,
6197
),
6298
scrollBehavior = behaviour,
6399
)
64100
}
101+
102+
@Composable
103+
private fun ToolbarTitle(title: ScaffoldTitle) {
104+
val contentTransform = remember {
105+
fadeIn(animationSpec = tween(220, delayMillis = 90))
106+
.togetherWith(fadeOut(animationSpec = tween(90)))
107+
}
108+
109+
AnimatedContent(
110+
modifier = Modifier.fillMaxWidth().heightIn(min = 24.dp),
111+
targetState = title,
112+
transitionSpec = { contentTransform },
113+
) { scaffoldTitle ->
114+
when (scaffoldTitle) {
115+
ScaffoldTitle.None -> {}
116+
is ScaffoldTitle.SearchBarTitle -> SearchBarTitle(scaffoldTitle)
117+
is ScaffoldTitle.TextTitle -> Text(
118+
text = scaffoldTitle.text,
119+
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
120+
textAlign = TextAlign.Center,
121+
)
122+
}
123+
}
124+
}
125+
126+
@OptIn(ExperimentalMaterialApi::class)
127+
@Composable
128+
private fun SearchBarTitle(title: ScaffoldTitle.SearchBarTitle) {
129+
val interactionSource = remember { MutableInteractionSource() }
130+
val focusRequester = remember { FocusRequester() }
131+
132+
LaunchedEffect(Unit) {
133+
focusRequester.requestFocus()
134+
}
135+
136+
BasicTextField(
137+
modifier = Modifier.fillMaxWidth().height(44.dp)
138+
.focusRequester(focusRequester),
139+
value = title.text.value,
140+
onValueChange = title.onTextUpdated,
141+
interactionSource = interactionSource,
142+
singleLine = true,
143+
textStyle = MaterialTheme.typography.bodySmall.copy(
144+
color = MaterialTheme.colorScheme.onBackground,
145+
fontSize = 16.sp,
146+
),
147+
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
148+
decorationBox = { innerTextField ->
149+
TextFieldDefaults.OutlinedTextFieldDecorationBox(
150+
value = title.text.value,
151+
contentPadding = TextFieldDefaults.textFieldWithoutLabelPadding(
152+
top = 8.dp,
153+
bottom = 8.dp,
154+
),
155+
innerTextField = innerTextField,
156+
placeholder = {
157+
Text(
158+
Strings.search,
159+
style = MaterialTheme.typography.bodySmall.copy(
160+
color = MaterialTheme.colorScheme.onBackground,
161+
fontSize = 16.sp,
162+
),
163+
)
164+
},
165+
enabled = true,
166+
singleLine = true,
167+
visualTransformation = VisualTransformation.None,
168+
interactionSource = interactionSource,
169+
colors = TextFieldDefaults.outlinedTextFieldColors(
170+
textColor = MaterialTheme.colorScheme.onSurface,
171+
focusedBorderColor = MaterialTheme.colorScheme.primary,
172+
unfocusedBorderColor = MaterialTheme.colorScheme.outline,
173+
),
174+
)
175+
},
176+
)
177+
}
65178
}

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/components/home/DealContent.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ object DealContent {
3838
@Composable
3939
operator fun invoke(
4040
state: HomeScreenState,
41+
searchTerm: String,
4142
onToggleFilterOption: (DealFilterOption) -> Unit,
4243
refreshAppDeals: () -> Unit,
4344
) {
44-
val deals = remember(state.allDeals, state.filterOptions) {
45+
val deals = remember(state.allDeals, state.filterOptions, searchTerm) {
4546
state.allDeals.filterWith(
4647
filterOptions = state.filterOptions,
4748
lastUpdatedTime = state.lastUpdatedTime,
49+
searchTerm = searchTerm,
4850
)
4951
}
5052

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/ChangeLogScreen.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences
3737
import me.sujanpoudel.playdeals.common.strings.Strings
3838
import me.sujanpoudel.playdeals.common.ui.components.ChangeLog
3939
import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold
40+
import me.sujanpoudel.playdeals.common.ui.components.common.rememberTextTitle
4041
import org.jetbrains.compose.resources.ExperimentalResourceApi
4142
import org.jetbrains.compose.resources.resource
4243
import org.kodein.di.direct
@@ -48,8 +49,8 @@ private const val CHANGELOG_PATH = "raw/changelog.md"
4849
@OptIn(ExperimentalResourceApi::class, ExperimentalFoundationApi::class)
4950
@Composable
5051
fun ChangeLogScreen() = Scaffold(
51-
title = Strings.changelog,
52-
showNavBackIcon = false,
52+
title = rememberTextTitle(Strings.changelog),
53+
showNavIcon = false,
5354
actions = {
5455
val outlineColor = MaterialTheme.colorScheme.outlineVariant
5556
IconButton(

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreen.kt

+39-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import androidx.compose.foundation.layout.absoluteOffset
55
import androidx.compose.foundation.layout.offset
66
import androidx.compose.foundation.layout.padding
77
import androidx.compose.foundation.layout.width
8+
import androidx.compose.material.icons.Icons
9+
import androidx.compose.material.icons.filled.Clear
10+
import androidx.compose.material.icons.filled.Search
811
import androidx.compose.material3.ExperimentalMaterial3Api
12+
import androidx.compose.material3.Icon
13+
import androidx.compose.material3.IconButton
914
import androidx.compose.material3.MaterialTheme
1015
import androidx.compose.material3.Scaffold
1116
import androidx.compose.material3.Snackbar
@@ -16,9 +21,12 @@ import androidx.compose.material3.rememberTopAppBarState
1621
import androidx.compose.runtime.Composable
1722
import androidx.compose.runtime.LaunchedEffect
1823
import androidx.compose.runtime.collectAsState
24+
import androidx.compose.runtime.derivedStateOf
1925
import androidx.compose.runtime.getValue
26+
import androidx.compose.runtime.mutableStateOf
2027
import androidx.compose.runtime.remember
2128
import androidx.compose.runtime.rememberCoroutineScope
29+
import androidx.compose.runtime.setValue
2230
import androidx.compose.ui.BiasAlignment
2331
import androidx.compose.ui.Modifier
2432
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -33,6 +41,7 @@ import me.sujanpoudel.playdeals.common.navigation.NavTransitions
3341
import me.sujanpoudel.playdeals.common.navigation.Navigator
3442
import me.sujanpoudel.playdeals.common.strings.Strings
3543
import me.sujanpoudel.playdeals.common.ui.components.common.ScaffoldToolbar
44+
import me.sujanpoudel.playdeals.common.ui.components.common.ScaffoldToolbar.ScaffoldTitle
3645
import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.PullRefreshIndicator
3746
import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.pullRefresh
3847
import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.rememberPullRefreshState
@@ -76,11 +85,39 @@ fun HomeScreen() {
7685
val topBarState = rememberTopAppBarState()
7786
val topBarScrollBehaviour = TopAppBarDefaults.enterAlwaysScrollBehavior(topBarState)
7887

88+
val searchTerm = viewModel.searchTerm.collectAsState()
89+
var showSearchToolbar by remember { mutableStateOf(false) }
90+
val toolbarTitle by remember {
91+
derivedStateOf {
92+
if (showSearchToolbar) {
93+
ScaffoldTitle.SearchBarTitle(searchTerm, viewModel::setSearchTerm)
94+
} else {
95+
ScaffoldTitle.TextTitle(strings.appDeals)
96+
}
97+
}
98+
}
99+
100+
LaunchedEffect(showSearchToolbar) {
101+
if (!showSearchToolbar) {
102+
delay(100)
103+
viewModel.setSearchTerm("")
104+
}
105+
}
106+
79107
Scaffold(
80108
topBar = {
81109
ScaffoldToolbar(
82-
title = Strings.appDeals,
110+
title = toolbarTitle,
83111
behaviour = topBarScrollBehaviour,
112+
alwaysShowNavIcon = true,
113+
navigationIcon = {
114+
IconButton(onClick = { showSearchToolbar = showSearchToolbar.not() }) {
115+
Icon(
116+
imageVector = if (showSearchToolbar) Icons.Default.Clear else Icons.Default.Search,
117+
contentDescription = "",
118+
)
119+
}
120+
},
84121
actions = {
85122
HomeScreen.NavMenu {
86123
coroutineScope.launch {
@@ -135,6 +172,7 @@ fun HomeScreen() {
135172
state,
136173
onToggleFilterOption = viewModel::toggleFilterItem,
137174
refreshAppDeals = viewModel::refreshDeals,
175+
searchTerm = searchTerm.value,
138176
)
139177
}
140178
}

shared/src/commonMain/kotlin/me/sujanpoudel/playdeals/common/ui/screens/home/HomeScreenState.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ data class HomeScreenState(
2323
fun List<DealEntity>.filterWith(
2424
filterOptions: List<Selectable<DealFilterOption>>,
2525
lastUpdatedTime: Instant,
26+
searchTerm: String,
2627
): List<DealEntity> {
2728
val selectedCategories = filterOptions
2829
.filter { it.data is DealFilterOption.Category && it.selected }
@@ -38,6 +39,11 @@ fun List<DealEntity>.filterWith(
3839
it is DealFilterOption.Category && it.value == deal.category
3940
}
4041

42+
val containsSearchTerm = searchTerm.isEmpty() || (
43+
deal.name.contains(searchTerm, ignoreCase = true) ||
44+
deal.category.contains(searchTerm, ignoreCase = true)
45+
)
46+
4147
val matchesOtherFilter = selectedOtherFilters.isEmpty() ||
4248
selectedOtherFilters.all {
4349
when (it) {
@@ -46,6 +52,6 @@ fun List<DealEntity>.filterWith(
4652
DealFilterOption.NewlyAddedApps -> deal.createdAt > lastUpdatedTime
4753
}
4854
}
49-
inSelectedCategory && matchesOtherFilter
55+
inSelectedCategory && matchesOtherFilter && containsSearchTerm
5056
}
5157
}

0 commit comments

Comments
 (0)