From 62ac35aea115bf38c3bc138b0467a06f0acaf326 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 04:54:31 +0000 Subject: [PATCH 1/3] Initial plan From fbbeecd22a9cced3052c9cff6fe002255b7c7fad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:04:23 +0000 Subject: [PATCH 2/3] Add sort order direction (ascending/descending) to search and wire up sort UI - Create SortOrder enum in domain layer with toGithubParam() method - Update SortBy.toGithubParams() to toGithubSortParam() returning only field name - Update SearchRepository interface and implementation to accept SortOrder - Add SortOrder to SearchState and SearchAction - Handle new sort actions in SearchViewModel - Update SortByBottomSheet with sort order toggle chips - Wire sort button and dialog into SearchRoot - Add string resources for sort order labels Co-authored-by: bilalahmadsheikh <169471620+bilalahmadsheikh@users.noreply.github.com> --- .../composeResources/values/strings.xml | 4 ++ .../data/repository/SearchRepositoryImpl.kt | 10 ++- .../kotlin/zed/rainxch/domain/model/SortBy.kt | 8 +-- .../zed/rainxch/domain/model/SortOrder.kt | 11 ++++ .../domain/repository/SearchRepository.kt | 3 + .../search/presentation/SearchAction.kt | 4 ++ .../rainxch/search/presentation/SearchRoot.kt | 63 +++++++++++++++++++ .../search/presentation/SearchState.kt | 4 ++ .../search/presentation/SearchViewModel.kt | 18 ++++++ .../components/SortByBottomSheet.kt | 40 +++++++++++- .../presentation/utils/SortOrderMapper.kt | 11 ++++ 11 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortOrder.kt create mode 100644 feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml index bb2b0e0bd..eca94739a 100644 --- a/core/presentation/src/commonMain/composeResources/values/strings.xml +++ b/core/presentation/src/commonMain/composeResources/values/strings.xml @@ -83,6 +83,10 @@ Most Forks Best Match + Descending + Ascending + Sort + All Languages Kotlin diff --git a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt index be069d6f5..27c81d921 100644 --- a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt +++ b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt @@ -30,6 +30,7 @@ import zed.rainxch.core.domain.model.RateLimitException import zed.rainxch.domain.model.ProgrammingLanguage import zed.rainxch.domain.model.SearchPlatform import zed.rainxch.domain.model.SortBy +import zed.rainxch.domain.model.SortOrder import zed.rainxch.domain.repository.SearchRepository import zed.rainxch.search.data.dto.GithubReleaseNetworkModel import zed.rainxch.search.data.utils.LruCache @@ -53,10 +54,11 @@ class SearchRepositoryImpl( platform: SearchPlatform, language: ProgrammingLanguage, sortBy: SortBy, + sortOrder: SortOrder, page: Int ): String { val queryHash = query.trim().lowercase().hashCode().toUInt().toString(16) - return "search:$queryHash:${platform.name}:${language.name}:${sortBy.name}:page$page" + return "search:$queryHash:${platform.name}:${language.name}:${sortBy.name}:${sortOrder.name}:page$page" } override fun searchRepositories( @@ -64,9 +66,10 @@ class SearchRepositoryImpl( searchPlatform: SearchPlatform, language: ProgrammingLanguage, sortBy: SortBy, + sortOrder: SortOrder, page: Int ): Flow = channelFlow { - val cacheKey = searchCacheKey(query, searchPlatform, language, sortBy, page) + val cacheKey = searchCacheKey(query, searchPlatform, language, sortBy, sortOrder, page) val cached = cacheManager.get(cacheKey) if (cached != null) { @@ -75,7 +78,8 @@ class SearchRepositoryImpl( } val searchQuery = buildSearchQuery(query, language) - val (sort, order) = sortBy.toGithubParams() + val sort = sortBy.toGithubSortParam() + val order = sortOrder.toGithubParam() try { var currentPage = page diff --git a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt index faf11d8c1..e7d2b7213 100644 --- a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt +++ b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortBy.kt @@ -5,9 +5,9 @@ enum class SortBy { MostForks, BestMatch; - fun toGithubParams(): Pair = when (this) { - MostStars -> "stars" to "desc" - MostForks -> "forks" to "desc" - BestMatch -> null to "desc" + fun toGithubSortParam(): String? = when (this) { + MostStars -> "stars" + MostForks -> "forks" + BestMatch -> null } } \ No newline at end of file diff --git a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortOrder.kt b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortOrder.kt new file mode 100644 index 000000000..3917c5f9e --- /dev/null +++ b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/model/SortOrder.kt @@ -0,0 +1,11 @@ +package zed.rainxch.domain.model + +enum class SortOrder { + Descending, + Ascending; + + fun toGithubParam(): String = when (this) { + Descending -> "desc" + Ascending -> "asc" + } +} diff --git a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt index c8301696a..88e6050ee 100644 --- a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt +++ b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt @@ -6,12 +6,15 @@ import zed.rainxch.domain.model.ProgrammingLanguage import zed.rainxch.domain.model.SearchPlatform import zed.rainxch.domain.model.SortBy +import zed.rainxch.domain.model.SortOrder + interface SearchRepository { fun searchRepositories( query: String, searchPlatform: SearchPlatform, language: ProgrammingLanguage, sortBy: SortBy, + sortOrder: SortOrder, page: Int ): Flow } \ No newline at end of file diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt index 72854a543..55ec4bdac 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt @@ -5,11 +5,14 @@ import zed.rainxch.domain.model.ProgrammingLanguage import zed.rainxch.domain.model.SearchPlatform import zed.rainxch.domain.model.SortBy +import zed.rainxch.domain.model.SortOrder + sealed interface SearchAction { data class OnSearchChange(val query: String) : SearchAction data class OnPlatformTypeSelected(val searchPlatform: SearchPlatform) : SearchAction data class OnLanguageSelected(val language: ProgrammingLanguage) : SearchAction data class OnSortBySelected(val sortBy: SortBy) : SearchAction + data class OnSortOrderSelected(val sortOrder: SortOrder) : SearchAction data class OnRepositoryClick(val repository: GithubRepoSummary) : SearchAction data class OnRepositoryDeveloperClick(val username: String) : SearchAction data class OnShareClick(val repo: GithubRepoSummary) : SearchAction @@ -20,6 +23,7 @@ sealed interface SearchAction { data object OnClearClick : SearchAction data object Retry : SearchAction data object OnToggleLanguageSheetVisibility : SearchAction + data object OnToggleSortByDialogVisibility : SearchAction data object OnFabClick : SearchAction data object DismissClipboardBanner : SearchAction } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index 2494f0ded..94bc23256 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Link @@ -100,7 +101,10 @@ import zed.rainxch.githubstore.core.presentation.res.open_in_app import zed.rainxch.githubstore.core.presentation.res.results_found import zed.rainxch.githubstore.core.presentation.res.retry import zed.rainxch.githubstore.core.presentation.res.search_repositories_hint +import zed.rainxch.githubstore.core.presentation.res.sort_label +import zed.rainxch.domain.model.SortBy import zed.rainxch.search.presentation.components.LanguageFilterBottomSheet +import zed.rainxch.search.presentation.components.SortByBottomSheet import zed.rainxch.search.presentation.utils.ParsedGithubLink import zed.rainxch.search.presentation.utils.label @@ -166,6 +170,23 @@ fun SearchRoot( } ) } + + if (state.isSortByDialogVisible) { + SortByBottomSheet( + sortByOptions = SortBy.entries, + selectedSortBy = state.selectedSortBy, + selectedSortOrder = state.selectedSortOrder, + onSortBySelected = { sortBy -> + viewModel.onAction(SearchAction.OnSortBySelected(sortBy)) + }, + onSortOrderSelected = { sortOrder -> + viewModel.onAction(SearchAction.OnSortOrderSelected(sortOrder)) + }, + onDismissRequest = { + viewModel.onAction(SearchAction.OnToggleSortByDialogVisibility) + } + ) + } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -399,6 +420,48 @@ fun SearchScreen( } } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(Res.string.sort_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.Medium + ) + + FilterChip( + selected = state.selectedSortBy != SortBy.BestMatch, + onClick = { + onAction(SearchAction.OnToggleSortByDialogVisibility) + }, + label = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Sort, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Text( + text = stringResource(state.selectedSortBy.label()), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Icon( + imageVector = Icons.Outlined.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + } + ) + } + Spacer(Modifier.height(6.dp)) if (state.totalCount != null) { diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt index 3e2f0b843..d8f71863f 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt @@ -6,11 +6,14 @@ import zed.rainxch.domain.model.SearchPlatform import zed.rainxch.domain.model.SortBy import zed.rainxch.search.presentation.utils.ParsedGithubLink +import zed.rainxch.domain.model.SortOrder + data class SearchState( val query: String = "", val repositories: List = emptyList(), val selectedSearchPlatform: SearchPlatform = SearchPlatform.All, val selectedSortBy: SortBy = SortBy.BestMatch, + val selectedSortOrder: SortOrder = SortOrder.Descending, val selectedLanguage: ProgrammingLanguage = ProgrammingLanguage.All, val isLoading: Boolean = false, val isLoadingMore: Boolean = false, @@ -18,6 +21,7 @@ data class SearchState( val hasMorePages: Boolean = true, val totalCount: Int? = null, val isLanguageSheetVisible: Boolean = false, + val isSortByDialogVisible: Boolean = false, val detectedLinks: List = emptyList(), val clipboardLinks: List = emptyList(), val isClipboardBannerVisible: Boolean = false, diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index 0761c67ad..49a5d4ccc 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -235,6 +235,7 @@ class SearchViewModel( searchPlatform = _state.value.selectedSearchPlatform, language = _state.value.selectedLanguage, sortBy = _state.value.selectedSortBy, + sortOrder = _state.value.selectedSortOrder, page = currentPage ) .collect { paginatedRepos -> @@ -447,6 +448,23 @@ class SearchViewModel( } } + is SearchAction.OnSortOrderSelected -> { + if (_state.value.selectedSortOrder != action.sortOrder) { + _state.update { + it.copy(selectedSortOrder = action.sortOrder) + } + currentPage = 1 + searchDebounceJob?.cancel() + performSearch(isInitial = true) + } + } + + SearchAction.OnToggleSortByDialogVisibility -> { + _state.update { + it.copy(isSortByDialogVisible = !it.isSortByDialogVisible) + } + } + SearchAction.LoadMore -> { if (!_state.value.isLoadingMore && !_state.value.isLoading && _state.value.hasMorePages) { performSearch(isInitial = false) diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt index 00a73d40c..1ef7f4fa8 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/components/SortByBottomSheet.kt @@ -4,21 +4,33 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilterChip +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.ui.Alignment import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import zed.rainxch.domain.model.SortBy +import zed.rainxch.domain.model.SortOrder +import zed.rainxch.githubstore.core.presentation.res.Res +import zed.rainxch.githubstore.core.presentation.res.close +import zed.rainxch.githubstore.core.presentation.res.sort_by import zed.rainxch.search.presentation.utils.label @Composable fun SortByBottomSheet( sortByOptions: List, selectedSortBy: SortBy, + selectedSortOrder: SortOrder, onSortBySelected: (SortBy) -> Unit, + onSortOrderSelected: (SortOrder) -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier ) { @@ -27,12 +39,12 @@ fun SortByBottomSheet( confirmButton = {}, dismissButton = { TextButton(onClick = onDismissRequest) { - Text(text = "Close") + Text(text = stringResource(Res.string.close)) } }, title = { Text( - text = "Sort by", + text = stringResource(Res.string.sort_by), style = MaterialTheme.typography.titleMedium ) }, @@ -46,7 +58,6 @@ fun SortByBottomSheet( TextButton( onClick = { onSortBySelected(option) - onDismissRequest() }, modifier = Modifier.fillMaxWidth() ) { @@ -57,6 +68,29 @@ fun SortByBottomSheet( ) } } + + HorizontalDivider() + + Spacer(Modifier.height(4.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + SortOrder.entries.forEach { order -> + FilterChip( + selected = order == selectedSortOrder, + onClick = { onSortOrderSelected(order) }, + label = { + Text( + text = stringResource(order.label()), + style = MaterialTheme.typography.bodyMedium + ) + } + ) + } + } } } ) diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt new file mode 100644 index 000000000..ae09feb44 --- /dev/null +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt @@ -0,0 +1,11 @@ +package zed.rainxch.search.presentation.utils + +import zed.rainxch.githubstore.core.presentation.res.* +import org.jetbrains.compose.resources.StringResource +import zed.rainxch.domain.model.SortOrder +import zed.rainxch.domain.model.SortOrder.* + +fun SortOrder.label(): StringResource = when (this) { + Descending -> Res.string.sort_order_descending + Ascending -> Res.string.sort_order_ascending +} From 6a728ae3b9c4e7f47711e216f480379d26811b84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:05:41 +0000 Subject: [PATCH 3/3] Fix import formatting from code review feedback Co-authored-by: bilalahmadsheikh <169471620+bilalahmadsheikh@users.noreply.github.com> --- .../kotlin/zed/rainxch/domain/repository/SearchRepository.kt | 1 - .../kotlin/zed/rainxch/search/presentation/SearchAction.kt | 1 - .../kotlin/zed/rainxch/search/presentation/SearchState.kt | 3 +-- .../zed/rainxch/search/presentation/utils/SortOrderMapper.kt | 5 +++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt index 88e6050ee..db7839db3 100644 --- a/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt +++ b/feature/search/domain/src/commonMain/kotlin/zed/rainxch/domain/repository/SearchRepository.kt @@ -5,7 +5,6 @@ import zed.rainxch.core.domain.model.PaginatedDiscoveryRepositories import zed.rainxch.domain.model.ProgrammingLanguage import zed.rainxch.domain.model.SearchPlatform import zed.rainxch.domain.model.SortBy - import zed.rainxch.domain.model.SortOrder interface SearchRepository { diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt index 55ec4bdac..e30901eed 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchAction.kt @@ -4,7 +4,6 @@ import zed.rainxch.core.domain.model.GithubRepoSummary import zed.rainxch.domain.model.ProgrammingLanguage import zed.rainxch.domain.model.SearchPlatform import zed.rainxch.domain.model.SortBy - import zed.rainxch.domain.model.SortOrder sealed interface SearchAction { diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt index d8f71863f..cc367672b 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt @@ -4,9 +4,8 @@ import zed.rainxch.core.presentation.model.DiscoveryRepository import zed.rainxch.domain.model.ProgrammingLanguage import zed.rainxch.domain.model.SearchPlatform import zed.rainxch.domain.model.SortBy -import zed.rainxch.search.presentation.utils.ParsedGithubLink - import zed.rainxch.domain.model.SortOrder +import zed.rainxch.search.presentation.utils.ParsedGithubLink data class SearchState( val query: String = "", diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt index ae09feb44..49e312651 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/utils/SortOrderMapper.kt @@ -1,9 +1,10 @@ package zed.rainxch.search.presentation.utils -import zed.rainxch.githubstore.core.presentation.res.* import org.jetbrains.compose.resources.StringResource import zed.rainxch.domain.model.SortOrder -import zed.rainxch.domain.model.SortOrder.* +import zed.rainxch.domain.model.SortOrder.Ascending +import zed.rainxch.domain.model.SortOrder.Descending +import zed.rainxch.githubstore.core.presentation.res.Res fun SortOrder.label(): StringResource = when (this) { Descending -> Res.string.sort_order_descending