Skip to content

Commit

Permalink
add search bar
Browse files Browse the repository at this point in the history
  • Loading branch information
psuzn committed Jan 5, 2024
1 parent 2021ede commit a3688d6
Show file tree
Hide file tree
Showing 14 changed files with 218 additions and 37 deletions.
1 change: 0 additions & 1 deletion desktopApp/src/jvmMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ interface AppStrings {
val permissionRequired: String
val grantPermission: String
val displayCurrencyDescription: String
val search: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ object StringEn : AppStrings {
override val moreInfo = "More Info"
override val aboutMe = "About me"
override val whatsNew = "What's New"
override val search = "Search"

// Settings Screen
override val appearance = "Appearance"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import me.sujanpoudel.playdeals.common.navigation.Navigator
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
title: String? = null,
showNavBackIcon: Boolean = true,
title: ScaffoldToolbar.ScaffoldTitle = ScaffoldToolbar.ScaffoldTitle.None,
showNavIcon: Boolean = true,
navigationIcon: @Composable (Navigator) -> Unit = { it -> ScaffoldToolbar.NavigationIcon(it) },
actions: (@Composable (Navigator) -> Unit)? = null,
content: @Composable BoxScope.() -> Unit,
Expand All @@ -23,7 +23,7 @@ fun Scaffold(
topBar = {
ScaffoldToolbar(
title = title,
showNavBackIcon = showNavBackIcon,
showNavIcon = showNavIcon,
actions = actions,
navigationIcon = navigationIcon,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
package me.sujanpoudel.playdeals.common.ui.components.common

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
Expand All @@ -12,54 +26,153 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.sujanpoudel.playdeals.common.navigation.Navigator
import me.sujanpoudel.playdeals.common.strings.Strings

@Composable
fun rememberTextTitle(title: String): ScaffoldToolbar.ScaffoldTitle {
return remember(title) {
ScaffoldToolbar.ScaffoldTitle.TextTitle(title)
}
}

object ScaffoldToolbar {

sealed class ScaffoldTitle {
data object None : ScaffoldTitle()

@Immutable
data class TextTitle(val text: String) : ScaffoldTitle()

@Immutable
class SearchBarTitle(val text: State<String>, val onTextUpdated: (String) -> Unit) : ScaffoldTitle()
}

@Composable
fun NavigationIcon(navigator: Navigator) {
IconButton(onClick = navigator::pop) {
Icon(Icons.Default.ArrowBack, contentDescription = "")
}
}

@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
operator fun invoke(
modifier: Modifier = Modifier,
title: String? = null,
showNavBackIcon: Boolean = true,
title: ScaffoldTitle,
showNavIcon: Boolean = true,
alwaysShowNavIcon: Boolean = false,
navigationIcon: @Composable (Navigator) -> Unit = { it -> NavigationIcon(it) },
actions: (@Composable (Navigator) -> Unit)? = null,
behaviour: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()),
) {
val navigator = Navigator.current

CenterAlignedTopAppBar(
modifier = modifier,
title = {
title?.let {
Text(
text = title,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
textAlign = TextAlign.Center,
)
}
},
navigationIcon = {
if (showNavBackIcon && navigator.backStackCount.value > 1) {
navigationIcon(navigator)
}
},
actions = { actions?.invoke(navigator) },
modifier = modifier.windowInsetsPadding(WindowInsets(top = 10.dp)),
title = { ToolbarTitle(title) },
navigationIcon =
{
if (alwaysShowNavIcon || (showNavIcon && navigator.backStackCount.value > 1)) {
navigationIcon(navigator)
}
},
actions =
{ actions?.invoke(navigator) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
),
scrollBehavior = behaviour,
)
}

@Composable
private fun ToolbarTitle(title: ScaffoldTitle) {
val contentTransform = remember {
fadeIn(animationSpec = tween(220, delayMillis = 90))
.togetherWith(fadeOut(animationSpec = tween(90)))
}

AnimatedContent(
modifier = Modifier.fillMaxWidth().heightIn(min = 24.dp),
targetState = title,
transitionSpec = { contentTransform },
) { scaffoldTitle ->
when (scaffoldTitle) {
ScaffoldTitle.None -> {}
is ScaffoldTitle.SearchBarTitle -> SearchBarTitle(scaffoldTitle)
is ScaffoldTitle.TextTitle -> Text(
text = scaffoldTitle.text,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
textAlign = TextAlign.Center,
)
}
}
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SearchBarTitle(title: ScaffoldTitle.SearchBarTitle) {
val interactionSource = remember { MutableInteractionSource() }
val focusRequester = remember { FocusRequester() }

LaunchedEffect(Unit) {
focusRequester.requestFocus()
}

BasicTextField(
modifier = Modifier.fillMaxWidth().height(44.dp)
.focusRequester(focusRequester),
value = title.text.value,
onValueChange = title.onTextUpdated,
interactionSource = interactionSource,
singleLine = true,
textStyle = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onBackground,
fontSize = 16.sp,
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
decorationBox = { innerTextField ->
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = title.text.value,
contentPadding = TextFieldDefaults.textFieldWithoutLabelPadding(
top = 8.dp,
bottom = 8.dp,
),
innerTextField = innerTextField,
placeholder = {
Text(
Strings.search,
style = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onBackground,
fontSize = 16.sp,
),
)
},
enabled = true,
singleLine = true,
visualTransformation = VisualTransformation.None,
interactionSource = interactionSource,
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = MaterialTheme.colorScheme.onSurface,
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline,
),
)
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ object DealContent {
@Composable
operator fun invoke(
state: HomeScreenState,
searchTerm: String,
onToggleFilterOption: (DealFilterOption) -> Unit,
refreshAppDeals: () -> Unit,
) {
val deals = remember(state.allDeals, state.filterOptions) {
val deals = remember(state.allDeals, state.filterOptions, searchTerm) {
state.allDeals.filterWith(
filterOptions = state.filterOptions,
lastUpdatedTime = state.lastUpdatedTime,
searchTerm = searchTerm,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import me.sujanpoudel.playdeals.common.domain.persistent.AppPreferences
import me.sujanpoudel.playdeals.common.strings.Strings
import me.sujanpoudel.playdeals.common.ui.components.ChangeLog
import me.sujanpoudel.playdeals.common.ui.components.common.Scaffold
import me.sujanpoudel.playdeals.common.ui.components.common.rememberTextTitle
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.resource
import org.kodein.di.direct
Expand All @@ -48,8 +49,8 @@ private const val CHANGELOG_PATH = "raw/changelog.md"
@OptIn(ExperimentalResourceApi::class, ExperimentalFoundationApi::class)
@Composable
fun ChangeLogScreen() = Scaffold(
title = Strings.changelog,
showNavBackIcon = false,
title = rememberTextTitle(Strings.changelog),
showNavIcon = false,
actions = {
val outlineColor = MaterialTheme.colorScheme.outlineVariant
IconButton(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Snackbar
Expand All @@ -16,9 +21,12 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
Expand All @@ -33,6 +41,7 @@ import me.sujanpoudel.playdeals.common.navigation.NavTransitions
import me.sujanpoudel.playdeals.common.navigation.Navigator
import me.sujanpoudel.playdeals.common.strings.Strings
import me.sujanpoudel.playdeals.common.ui.components.common.ScaffoldToolbar
import me.sujanpoudel.playdeals.common.ui.components.common.ScaffoldToolbar.ScaffoldTitle
import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.PullRefreshIndicator
import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.pullRefresh
import me.sujanpoudel.playdeals.common.ui.components.common.pullToRefresh.rememberPullRefreshState
Expand Down Expand Up @@ -76,11 +85,39 @@ fun HomeScreen() {
val topBarState = rememberTopAppBarState()
val topBarScrollBehaviour = TopAppBarDefaults.enterAlwaysScrollBehavior(topBarState)

val searchTerm = viewModel.searchTerm.collectAsState()
var showSearchToolbar by remember { mutableStateOf(false) }
val toolbarTitle by remember {
derivedStateOf {
if (showSearchToolbar) {
ScaffoldTitle.SearchBarTitle(searchTerm, viewModel::setSearchTerm)
} else {
ScaffoldTitle.TextTitle(strings.appDeals)
}
}
}

LaunchedEffect(showSearchToolbar) {
if (!showSearchToolbar) {
delay(100)
viewModel.setSearchTerm("")
}
}

Scaffold(
topBar = {
ScaffoldToolbar(
title = Strings.appDeals,
title = toolbarTitle,
behaviour = topBarScrollBehaviour,
alwaysShowNavIcon = true,
navigationIcon = {
IconButton(onClick = { showSearchToolbar = showSearchToolbar.not() }) {
Icon(
imageVector = if (showSearchToolbar) Icons.Default.Clear else Icons.Default.Search,
contentDescription = "",
)
}
},
actions = {
HomeScreen.NavMenu {
coroutineScope.launch {
Expand Down Expand Up @@ -135,6 +172,7 @@ fun HomeScreen() {
state,
onToggleFilterOption = viewModel::toggleFilterItem,
refreshAppDeals = viewModel::refreshDeals,
searchTerm = searchTerm.value,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data class HomeScreenState(
fun List<DealEntity>.filterWith(
filterOptions: List<Selectable<DealFilterOption>>,
lastUpdatedTime: Instant,
searchTerm: String,
): List<DealEntity> {
val selectedCategories = filterOptions
.filter { it.data is DealFilterOption.Category && it.selected }
Expand All @@ -38,6 +39,11 @@ fun List<DealEntity>.filterWith(
it is DealFilterOption.Category && it.value == deal.category
}

val containsSearchTerm = searchTerm.isEmpty() || (
deal.name.contains(searchTerm, ignoreCase = true) ||
deal.category.contains(searchTerm, ignoreCase = true)
)

val matchesOtherFilter = selectedOtherFilters.isEmpty() ||
selectedOtherFilters.all {
when (it) {
Expand All @@ -46,6 +52,6 @@ fun List<DealEntity>.filterWith(
DealFilterOption.NewlyAddedApps -> deal.createdAt > lastUpdatedTime
}
}
inSelectedCategory && matchesOtherFilter
inSelectedCategory && matchesOtherFilter && containsSearchTerm
}
}
Loading

0 comments on commit a3688d6

Please sign in to comment.