Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.itsaky.androidide.fragments

import android.animation.ValueAnimator
import android.os.Bundle
import android.transition.AutoTransition
import android.transition.TransitionManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
Expand All @@ -13,6 +18,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.itsaky.androidide.R
import com.itsaky.androidide.activities.MainActivity
Expand All @@ -28,6 +34,7 @@ import com.itsaky.androidide.utils.Environment.PROJECTS_DIR
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.viewLifecycleScope
import com.itsaky.androidide.viewmodel.MainViewModel
import com.itsaky.androidide.viewmodel.FilterState
import com.itsaky.androidide.viewmodel.RecentProjectsViewModel
import com.itsaky.androidide.viewmodel.SortCriteria
import com.itsaky.androidide.ui.ProjectInfoBottomSheet
Expand Down Expand Up @@ -56,6 +63,7 @@ class RecentProjectsFragment : BaseFragment() {
private var selectedCriteria: SortCriteria? = null
private var selectedAsc = true
private val searchQuery = MutableStateFlow("")
private var filtersDialog: BottomSheetDialog? = null

data class SortToggleStyle(
val iconRes: Int,
Expand All @@ -80,6 +88,7 @@ class RecentProjectsFragment : BaseFragment() {
setupSearchBar()
setupObservers()
setupClickListeners()
setupFilterChips()
bootstrapFromFixedFolderIfNeeded()
observeDeletionStatus()
observeRenameStatus()
Expand All @@ -99,10 +108,8 @@ class RecentProjectsFragment : BaseFragment() {
dialog.setContentView(sheet)
setupFilters(sheet)

viewLifecycleScope.launch {
viewModel.filterEvents.collect { dialog.dismiss() }
}

dialog.setOnDismissListener { filtersDialog = null }
filtersDialog = dialog
dialog.show()
}

Expand Down Expand Up @@ -179,12 +186,7 @@ class RecentProjectsFragment : BaseFragment() {
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
}
val labelRes = selectedCriteria?.labelRes()

if (labelRes != null) {
sortDropdown.setText(getString(labelRes), false)
Expand Down Expand Up @@ -222,6 +224,93 @@ class RecentProjectsFragment : BaseFragment() {
setupSortToggle(button, selectedAsc)
}

/**
* Reflects the active sort/search as removable chips and toggles the filter button's
* active dot. Driven by the view model so it stays in sync with the filters sheet,
* the search bar, and clearing.
*/
private fun setupFilterChips() {
viewLifecycleScope.launch {
viewModel.filterState.collect { renderActiveFilters(it) }
}
viewLifecycleScope.launch {
viewModel.filterEvents.collect { filtersDialog?.dismiss() }
}
}

private fun renderActiveFilters(state: FilterState) {
val filters = _binding?.layoutFilters ?: return
val group = filters.activeFiltersGroup

beginFilterBarTransition(filters.root as? ViewGroup)

group.removeAllViews()

state.sort?.let { criteria ->
val arrow = if (state.ascending) "↑" else "↓"
group.addView(
buildFilterChip(
text = "${getString(criteria.labelRes())} $arrow",
removeDescRes = R.string.filter_chip_remove_sort,
onClick = { openFiltersSheet() },
) {
viewLifecycleScope.launch { viewModel.clearSort() }
},
)
}

if (state.query.isNotEmpty()) {
group.addView(
buildFilterChip(
text = "“${state.query}”",
removeDescRes = R.string.filter_chip_remove_search,
onClick = { focusSearchField() },
) {
filters.searchProjectEditText.text?.clear()
Comment thread
Daniel-ADFA marked this conversation as resolved.
},
)
}

filters.activeFiltersScroll.isVisible = group.childCount > 0
filters.filtersActiveDot.isVisible = state.hasAny
filters.openFiltersBtn.contentDescription = if (state.hasAny) {
"${getString(R.string.sort_projects_label)}, ${getString(R.string.filters_active)}"
} else {
getString(R.string.sort_projects_label)
}
}

private fun focusSearchField() {
val editText = binding.layoutFilters.searchProjectEditText
editText.requestFocus()
ContextCompat.getSystemService(requireContext(), InputMethodManager::class.java)
?.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}

private fun buildFilterChip(
text: String,
removeDescRes: Int,
onClick: () -> Unit,
onRemove: () -> Unit,
): Chip {
val chip = layoutInflater.inflate(
R.layout.chip_active_filter,
binding.layoutFilters.activeFiltersGroup,
false,
) as Chip
chip.text = text
chip.closeIconContentDescription = getString(removeDescRes)
chip.setOnCloseIconClickListener { onRemove() }
chip.setOnClickListener { onClick() }
return chip
}

private fun beginFilterBarTransition(scene: ViewGroup?) {
if (scene != null && ValueAnimator.areAnimatorsEnabled()) {
TransitionManager.beginDelayedTransition(scene, AutoTransition().setDuration(180))
}
}




Expand Down Expand Up @@ -442,3 +531,10 @@ class RecentProjectsFragment : BaseFragment() {
}

}

@StringRes
private fun SortCriteria.labelRes(): Int = when (this) {
SortCriteria.NAME -> R.string.sort_by_name
SortCriteria.DATE_CREATED -> R.string.sort_by_created
SortCriteria.DATE_MODIFIED -> R.string.sort_by_modified
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import org.appdevforall.codeonthego.layouteditor.ProjectFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
Expand All @@ -31,6 +34,14 @@ enum class SortCriteria {
DATE_MODIFIED
}

data class FilterState(
val query: String = "",
val sort: SortCriteria? = null,
val ascending: Boolean = true
) {
val hasAny: Boolean get() = sort != null || query.isNotEmpty()
}

class RecentProjectsViewModel(application: Application) : AndroidViewModel(application) {

companion object {
Expand All @@ -47,10 +58,13 @@ class RecentProjectsViewModel(application: Application) : AndroidViewModel(appli
private var currentSort: SortCriteria? = null
private var isAscending: Boolean = true

private val _filterState = MutableStateFlow(FilterState())
val filterState: StateFlow<FilterState> = _filterState.asStateFlow()

val currentSortCriteria: SortCriteria? get() = currentSort
val currentSortAscending: Boolean get() = isAscending
val hasActiveFilters: Boolean
get() = currentSort != null || !isAscending || currentQuery.isNotEmpty()
get() = _filterState.value.hasAny

private val _deletionStatus = MutableSharedFlow<Boolean>(replay = 1)
val deletionStatus = _deletionStatus.asSharedFlow()
Expand Down Expand Up @@ -80,19 +94,20 @@ class RecentProjectsViewModel(application: Application) : AndroidViewModel(appli
}

private suspend fun applyFilters() {
_filterState.value = FilterState(currentQuery, currentSort, isAscending)
withContext(Dispatchers.Default) {
var result = allProjects

if (currentQuery.isNotEmpty()) {
result = result.filter { it.name.contains(currentQuery, ignoreCase = true) }
}

currentSort.let { criteria ->
val criteria = currentSort
if (criteria != null) {
result = when (criteria) {
SortCriteria.NAME -> result.sortedBy { it.name.lowercase() }
SortCriteria.DATE_CREATED -> result.sortedBy { it.createdAt }
SortCriteria.DATE_MODIFIED -> result.sortedBy { it.lastModified }
else -> result
}
if (!isAscending) {
result = result.reversed()
Expand Down Expand Up @@ -124,6 +139,12 @@ class RecentProjectsViewModel(application: Application) : AndroidViewModel(appli
applyFilters()
}

suspend fun clearSort() {
currentSort = null
isAscending = true
applyFilters()
}

suspend fun getProjectByName(name: String): RecentProject? {
return withContext(Dispatchers.IO) {
recentProjectDao.getProjectByName(name)
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/res/layout/chip_active_filter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Material3.Chip.Input"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/colorOnSurface"
app:chipBackgroundColor="?attr/colorSurface"
app:chipStrokeColor="?attr/colorOutline"
app:chipStrokeWidth="1dp"
app:closeIcon="@drawable/ic_close"
app:closeIconTint="?attr/colorOnSurfaceVariant"
app:closeIconVisible="true" />
85 changes: 61 additions & 24 deletions app/src/main/res/layout/layout_project_filters_bar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,70 @@
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:orientation="vertical"
android:paddingHorizontal="8dp">

<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"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search_projects_hint">
android:orientation="horizontal"
android:gravity="center_vertical">

<com.google.android.material.textfield.TextInputEditText
android:id="@+id/search_project_edit_text"
android:layout_width="match_parent"
<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: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: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>

<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp">

<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:tooltipText="@string/sort_projects_label"
app:icon="@drawable/ic_sort" />

<View
android:id="@+id/filters_active_dot"
Comment thread
Daniel-ADFA marked this conversation as resolved.
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_gravity="top|end"
android:layout_margin="4dp"
android:importantForAccessibility="no"
android:background="@drawable/bg_filter_active_dot"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

<HorizontalScrollView
android:id="@+id/active_filters_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:tooltipText="@string/sort_projects_label"
app:icon="@drawable/ic_sort" />
</LinearLayout>
android:layout_marginTop="8dp"
android:scrollbars="none"
android:visibility="gone">

<com.google.android.material.chip.ChipGroup
android:id="@+id/active_filters_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingHorizontal="6dp"
app:singleLine="true" />
</HorizontalScrollView>
</LinearLayout>
11 changes: 11 additions & 0 deletions resources/src/main/res/drawable/bg_filter_active_dot.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?attr/colorPrimary" />
<stroke
android:width="1.5dp"
android:color="?attr/colorSurface" />
<size
android:width="10dp"
android:height="10dp" />
</shape>
3 changes: 3 additions & 0 deletions resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@
<string name="sort_by_name">Name</string>
<string name="sort_by_created">Created</string>
<string name="sort_by_modified">Edited</string>
<string name="filters_active">Filters active</string>
<string name="filter_chip_remove_sort">Remove sort</string>
<string name="filter_chip_remove_search">Remove search filter</string>
<string-array name="sort_options">
<item>@string/sort_by_name</item>
<item>@string/sort_by_created</item>
Expand Down
Loading