Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 55
ADFA-2247 | Add search, sorting, and filtering UI for saved projects#700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
a60f3ee828683a594f6608261a5dbfedf528cddc0e705492910a579d140e5a76e05c9ea97bbf1bb06915f907e67c66a55e9f0c6b277d116a34ca088243d105b301ec20be855f2e66648830a978File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4,9 +4,14 @@ import android.os.Bundle | ||
| import android.view.LayoutInflater | ||
| import android.view.View | ||
| import android.view.ViewGroup | ||
| import androidx.core.content.ContextCompat | ||
| import androidx.core.view.isVisible | ||
| import androidx.core.widget.addTextChangedListener | ||
| import androidx.fragment.app.activityViewModels | ||
| import androidx.recyclerview.widget.LinearLayoutManager | ||
| import com.google.android.material.bottomsheet.BottomSheetDialog | ||
| import com.google.android.material.button.MaterialButton | ||
| import com.google.android.material.textfield.MaterialAutoCompleteTextView | ||
| import com.itsaky.androidide.R | ||
| import com.itsaky.androidide.activities.MainActivity | ||
| import com.itsaky.androidide.adapters.RecentProjectsAdapter | ||
| @@ -23,8 +28,11 @@ import com.itsaky.androidide.utils.viewLifecycleScope | ||
| import com.itsaky.androidide.viewmodel.MainViewModel | ||
| import com.itsaky.androidide.viewmodel.RecentProjectsViewModel | ||
| import com.itsaky.androidide.preferences.internal.GeneralPreferences | ||
| import com.itsaky.androidide.viewmodel.SortCriteria | ||
| import io.sentry.Sentry | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.debounce | ||
| import kotlinx.coroutines.joinAll | ||
| import kotlinx.coroutines.launch | ||
| import kotlinx.coroutines.withContext | ||
| @@ -38,6 +46,14 @@ class RecentProjectsFragment : BaseFragment() { | ||
| private val viewModel: RecentProjectsViewModel by activityViewModels() | ||
| private val mainViewModel: MainViewModel by activityViewModels() | ||
| private lateinit var adapter: RecentProjectsAdapter | ||
| private var selectedCriteria: SortCriteria? = null | ||
| private var selectedAsc = true | ||
| private val searchQuery = MutableStateFlow("") | ||
| data class SortToggleStyle( | ||
| val iconRes: Int, | ||
| val colorRes: Int | ||
| ) | ||
| override fun onCreateView( | ||
| inflater: LayoutInflater, | ||
| @@ -54,6 +70,7 @@ class RecentProjectsFragment : BaseFragment() { | ||
| ) { | ||
| super.onViewCreated(view, savedInstanceState) | ||
| setupRecyclerView() | ||
| setupSearchBar() | ||
| setupObservers() | ||
| setupClickListeners() | ||
| bootstrapFromFixedFolderIfNeeded() | ||
| @@ -66,6 +83,137 @@ class RecentProjectsFragment : BaseFragment() { | ||
| ) | ||
| } | ||
| private fun openFiltersSheet() { | ||
| val dialog = BottomSheetDialog(requireContext()) | ||
| val sheet = layoutInflater.inflate(R.layout.layout_project_filters_sheet, null) | ||
| dialog.setContentView(sheet) | ||
| setupFilters(sheet) | ||
| viewLifecycleScope.launch { | ||
| viewModel.filterEvents.collect { dialog.dismiss() } | ||
| } | ||
| dialog.show() | ||
| } | ||
| @OptIn(kotlinx.coroutines.FlowPreview::class) | ||
| private fun setupSearchBar() { | ||
| viewLifecycleScope.launch { | ||
| searchQuery | ||
| .debounce(300) | ||
| .collect { query -> viewModel.onSearchQuery(query) } | ||
| } | ||
| binding.layoutFilters.searchProjectEditText.addTextChangedListener { text -> | ||
| searchQuery.value = text?.toString().orEmpty() | ||
| } | ||
| } | ||
| private fun setupFilters(sheet: View) { | ||
| val sortDropdown = sheet.requireViewById<MaterialAutoCompleteTextView>(R.id.sort_dropdown) | ||
| val sortToggleBtn = sheet.requireViewById<MaterialButton>(R.id.sort_toggle_btn) | ||
| val applyBtn = sheet.requireViewById<MaterialButton>(R.id.apply_filters_btn) | ||
| val clearBtn = sheet.requireViewById<MaterialButton>(R.id.clear_filters_btn) | ||
| selectedCriteria = viewModel.currentSortCriteria | ||
| selectedAsc = viewModel.currentSortAscending | ||
| setupSortUI(sortDropdown, sortToggleBtn) | ||
| clearBtn.isVisible = viewModel.hasActiveFilters | ||
| sortDropdown.setOnItemClickListener { _, _, position, _ -> | ||
| selectedCriteria = when (position) { | ||
| 0 -> SortCriteria.NAME | ||
| 1 -> SortCriteria.DATE_CREATED | ||
| 2 -> SortCriteria.DATE_MODIFIED | ||
| else -> SortCriteria.NAME | ||
| } | ||
| clearBtn.isVisible = true | ||
| } | ||
| sortToggleBtn.setOnClickListener { | ||
| toggleSortDirection(sortToggleBtn) | ||
| clearBtn.isVisible = true | ||
| } | ||
| applyBtn.setOnClickListener { | ||
| viewLifecycleScope.launch { | ||
| selectedCriteria?.let { viewModel.onSortSelected(it) } | ||
| viewModel.onSortDirectionChanged(selectedAsc) | ||
| } | ||
| viewModel.notifyFiltersSaved() | ||
| } | ||
| clearBtn.setOnClickListener { | ||
| selectedCriteria = null | ||
| selectedAsc = true | ||
| sortDropdown.text = null | ||
| setupSortToggle(sortToggleBtn, true) | ||
| binding.layoutFilters.searchProjectEditText.text?.clear() | ||
| clearBtn.isVisible = false | ||
| viewLifecycleScope.launch { | ||
| viewModel.clearFilters() | ||
| } | ||
| viewModel.notifyFiltersSaved() | ||
| } | ||
jatezzz marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| /** | ||
| * Initializes UI according to current VM state. | ||
| */ | ||
| private fun setupSortUI( | ||
| sortDropdown: MaterialAutoCompleteTextView, | ||
| sortToggleBtn: MaterialButton | ||
| ) { | ||
| val labelRes = when (selectedCriteria) { | ||
| SortCriteria.NAME -> R.string.sort_by_name | ||
| SortCriteria.DATE_CREATED -> R.string.sort_by_created | ||
| SortCriteria.DATE_MODIFIED -> R.string.sort_by_modified | ||
| null -> null | ||
| } | ||
| if (labelRes != null) { | ||
| sortDropdown.setText(getString(labelRes), false) | ||
| } else { | ||
| sortDropdown.text = null | ||
| } | ||
| setupSortToggle(sortToggleBtn, selectedAsc) | ||
| } | ||
| /** | ||
| * Updates the arrow icon, the background color and the text based on ascending state. | ||
| */ | ||
| private fun setupSortToggle(button: MaterialButton, asc: Boolean) { | ||
| val style = if (asc) { | ||
| SortToggleStyle( | ||
| R.drawable.ic_arrow_up, R.color._blue_wave_light_colorPrimaryDark | ||
| ) | ||
| } else { | ||
| SortToggleStyle( | ||
| R.drawable.ic_arrow_down, R.color._blue_wave_dark_colorOnSecondary | ||
| ) | ||
| } | ||
| button.apply { | ||
| setIconResource(style.iconRes) | ||
| backgroundTintList = ContextCompat.getColorStateList(context, style.colorRes) | ||
| } | ||
| } | ||
| /** | ||
| * Toggles the ascending/descending state and updates the UI. | ||
| */ | ||
| private fun toggleSortDirection(button: MaterialButton) { | ||
| selectedAsc = !selectedAsc | ||
| setupSortToggle(button, selectedAsc) | ||
| } | ||
| private fun File.isProjectCandidateDir(): Boolean = isDirectory && canRead() && !name.startsWith(".") && !isHidden | ||
| private fun bootstrapFromFixedFolderIfNeeded() { | ||
| @@ -289,6 +437,9 @@ class RecentProjectsFragment : BaseFragment() { | ||
| showToolTip(PROJECT_RECENT_TOP) | ||
| true | ||
| } | ||
| binding.layoutFilters.openFiltersBtn.setOnClickListener { | ||
| openFiltersSheet() | ||
| } | ||
| } | ||
| private fun openProject(root: File) { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <LinearLayout | ||
| xmlns:android="http://schemas.android.com/apk/res/android" | ||
| xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="wrap_content" | ||
| android:orientation="horizontal" | ||
| android:gravity="center_vertical" | ||
| android:paddingHorizontal="16dp" | ||
| android:layout_marginTop="12dp"> | ||
| <com.google.android.material.textfield.TextInputLayout | ||
| android:id="@+id/search_project_input_layout" | ||
| style="@style/Widget.Material3.TextInputLayout.OutlinedBox" | ||
| android:layout_width="0dp" | ||
| android:layout_weight="1" | ||
| android:layout_height="wrap_content" | ||
| android:hint="@string/search_projects_hint"> | ||
| <com.google.android.material.textfield.TextInputEditText | ||
| android:id="@+id/search_project_edit_text" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="wrap_content" | ||
| android:imeOptions="actionSearch" | ||
| android:singleLine="true" /> | ||
| </com.google.android.material.textfield.TextInputLayout> | ||
| <com.google.android.material.button.MaterialButton | ||
| android:id="@+id/open_filters_btn" | ||
| android:contentDescription="@string/sort_projects_label" | ||
| style="@style/Widget.Material3.Button.IconButton.Filled" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:layout_marginStart="6dp" | ||
| app:icon="@drawable/ic_sort" /> | ||
| </LinearLayout> | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.