feat: Toggle display of search operators with toolbar action (#836)

Default to hiding the search operators, and provide a new toolbar icon
(always visible) to show them.

The toolbar icon is displayed with a badge if any operators are present.

Adjust the operator display to three horizontal scrolling rows, to
further limit the maximum amount of vertical space the operators use.
This commit is contained in:
Nik Clayton 2024-07-24 18:51:00 +02:00 committed by GitHub
parent 5d574d4d76
commit 01831474dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 336 additions and 135 deletions

View File

@ -105,7 +105,7 @@
line="388" line="388"
column="28"/> column="28"/>
<location <location
file="${:core:activity*buildDir}/generated/res/resValues/orangeFdroid/debug/values/gradleResValues.xml" file="${:core:activity*buildDir}/generated/res/resValues/blueFdroid/debug/values/gradleResValues.xml"
line="7" line="7"
column="5" column="5"
message="This definition does not require arguments"/> message="This definition does not require arguments"/>
@ -767,7 +767,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="282" line="283"
column="5"/> column="5"/>
</issue> </issue>
@ -778,7 +778,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="333" line="334"
column="5"/> column="5"/>
</issue> </issue>
@ -789,7 +789,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="452" line="453"
column="5"/> column="5"/>
</issue> </issue>
@ -800,7 +800,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="628" line="629"
column="5"/> column="5"/>
</issue> </issue>
@ -1196,7 +1196,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/donottranslate.xml" file="src/main/res/values/donottranslate.xml"
line="273" line="275"
column="19"/> column="19"/>
</issue> </issue>
@ -1207,7 +1207,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/donottranslate.xml" file="src/main/res/values/donottranslate.xml"
line="278" line="280"
column="19"/> column="19"/>
</issue> </issue>
@ -1372,7 +1372,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="198" line="199"
column="13"/> column="13"/>
</issue> </issue>
@ -1383,7 +1383,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="219" line="220"
column="13"/> column="13"/>
</issue> </issue>
@ -1394,7 +1394,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="220" line="221"
column="13"/> column="13"/>
</issue> </issue>
@ -1405,7 +1405,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="243" line="244"
column="13"/> column="13"/>
</issue> </issue>
@ -1416,7 +1416,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="272" line="273"
column="13"/> column="13"/>
</issue> </issue>
@ -1427,7 +1427,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="343" line="344"
column="13"/> column="13"/>
</issue> </issue>
@ -1438,7 +1438,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="383" line="384"
column="13"/> column="13"/>
</issue> </issue>
@ -1449,7 +1449,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="419" line="420"
column="13"/> column="13"/>
</issue> </issue>
@ -1460,7 +1460,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="422" line="423"
column="13"/> column="13"/>
</issue> </issue>
@ -1471,7 +1471,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="423" line="424"
column="13"/> column="13"/>
</issue> </issue>
@ -1482,7 +1482,7 @@
errorLine2=" ~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="424" line="425"
column="13"/> column="13"/>
</issue> </issue>
@ -1493,7 +1493,7 @@
errorLine2=" ~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="425" line="426"
column="13"/> column="13"/>
</issue> </issue>
@ -1504,7 +1504,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="426" line="427"
column="13"/> column="13"/>
</issue> </issue>
@ -1515,7 +1515,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="438" line="439"
column="13"/> column="13"/>
</issue> </issue>
@ -1526,7 +1526,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="439" line="440"
column="13"/> column="13"/>
</issue> </issue>
@ -1537,7 +1537,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="490" line="491"
column="13"/> column="13"/>
</issue> </issue>
@ -1548,7 +1548,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="491" line="492"
column="13"/> column="13"/>
</issue> </issue>
@ -1559,7 +1559,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="502" line="503"
column="13"/> column="13"/>
</issue> </issue>
@ -1570,7 +1570,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="503" line="504"
column="13"/> column="13"/>
</issue> </issue>
@ -1581,7 +1581,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="504" line="505"
column="13"/> column="13"/>
</issue> </issue>
@ -1592,7 +1592,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="507" line="508"
column="13"/> column="13"/>
</issue> </issue>
@ -1603,7 +1603,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="545" line="546"
column="13"/> column="13"/>
</issue> </issue>
@ -1614,7 +1614,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="586" line="587"
column="13"/> column="13"/>
</issue> </issue>
@ -1625,7 +1625,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="592" line="593"
column="13"/> column="13"/>
</issue> </issue>
@ -1636,7 +1636,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="620" line="621"
column="13"/> column="13"/>
</issue> </issue>
@ -1647,7 +1647,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/strings.xml" file="src/main/res/values/strings.xml"
line="646" line="647"
column="13"/> column="13"/>
</issue> </issue>

View File

@ -25,6 +25,7 @@ import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
@ -70,6 +71,7 @@ import app.pachli.components.search.adapter.SearchPagerAdapter
import app.pachli.core.activity.BottomSheetActivity import app.pachli.core.activity.BottomSheetActivity
import app.pachli.core.common.extensions.hide import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.toggleVisibility
import app.pachli.core.common.extensions.viewBinding import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.common.extensions.visible import app.pachli.core.common.extensions.visible
import app.pachli.core.network.Server import app.pachli.core.network.Server
@ -98,7 +100,11 @@ import app.pachli.databinding.SearchOperatorDateDialogBinding
import app.pachli.databinding.SearchOperatorFromDialogBinding import app.pachli.databinding.SearchOperatorFromDialogBinding
import app.pachli.databinding.SearchOperatorWhereLocationDialogBinding import app.pachli.databinding.SearchOperatorWhereLocationDialogBinding
import com.github.michaelbull.result.get import com.github.michaelbull.result.get
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.google.android.material.datepicker.CalendarConstraints import com.google.android.material.datepicker.CalendarConstraints
import com.google.android.material.datepicker.DateValidatorPointBackward import com.google.android.material.datepicker.DateValidatorPointBackward
import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.datepicker.MaterialDatePicker
@ -125,6 +131,9 @@ class SearchActivity :
private lateinit var searchView: SearchView private lateinit var searchView: SearchView
val showFilterIcon: Boolean
get() = viewModel.availableOperators.value.isNotEmpty()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
@ -156,6 +165,7 @@ class SearchActivity :
* Binds the initial search operator chips UI and updates as the search * Binds the initial search operator chips UI and updates as the search
* operators change. * operators change.
*/ */
@OptIn(ExperimentalBadgeUtils::class)
private fun bindOperators() { private fun bindOperators() {
val viewDataToChip: Map<Class<out SearchOperatorViewData<SearchOperator>>, Chip> = mapOf( val viewDataToChip: Map<Class<out SearchOperatorViewData<SearchOperator>>, Chip> = mapOf(
DateOperatorViewData::class.java to binding.chipDate, DateOperatorViewData::class.java to binding.chipDate,
@ -170,6 +180,18 @@ class SearchActivity :
WhereOperatorViewData::class.java to binding.chipWhere, WhereOperatorViewData::class.java to binding.chipWhere,
) )
// Chips are initially hidden, toggled by the "filter" button
binding.chipsFilter.hide()
binding.chipsFilter2.hide()
binding.chipsFilter3.hide()
// Badge to draw on the filter button if any filters are active.
val filterBadgeDrawable = BadgeDrawable.create(this).apply {
text = "!"
backgroundColor = MaterialColors.getColor(binding.toolbar, com.google.android.material.R.attr.colorPrimary)
}
BadgeUtils.attachBadgeDrawable(filterBadgeDrawable, binding.toolbar, R.id.action_filter_search)
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch { launch {
@ -189,17 +211,28 @@ class SearchActivity :
} }
} }
launch {
viewModel.availableOperators.collectLatest {
invalidateOptionsMenu()
setSearchViewWidth(showFilterIcon)
}
}
launch { launch {
viewModel.operatorViewData.collectLatest { operators -> viewModel.operatorViewData.collectLatest { operators ->
var showFilterBadgeDrawable = false
operators.forEach { viewData -> operators.forEach { viewData ->
viewDataToChip[viewData::class.java]?.let { chip -> viewDataToChip[viewData::class.java]?.let { chip ->
showFilterBadgeDrawable = showFilterBadgeDrawable or (viewData.operator.choice != null)
chip.isChecked = viewData.operator.choice != null chip.isChecked = viewData.operator.choice != null
chip.setCloseIconVisible(viewData.operator.choice != null) chip.setCloseIconVisible(viewData.operator.choice != null)
chip.text = viewData.chipLabel(this@SearchActivity) chip.text = viewData.chipLabel(this@SearchActivity)
} }
viewModel.search()
} }
filterBadgeDrawable.setVisible(showFilterBadgeDrawable)
viewModel.search()
} }
} }
} }
@ -933,15 +966,35 @@ class SearchActivity :
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
super.onCreateMenu(menu, menuInflater) super.onCreateMenu(menu, menuInflater)
menuInflater.inflate(R.menu.search_toolbar, menu) menuInflater.inflate(R.menu.search_toolbar, menu)
menu.findItem(R.id.action_filter_search)?.apply {
icon = makeIcon(this@SearchActivity, GoogleMaterial.Icon.gmd_tune, IconicsSize.dp(20))
}
val searchViewMenuItem = menu.findItem(R.id.action_search) val searchViewMenuItem = menu.findItem(R.id.action_search)
searchViewMenuItem.expandActionView() searchViewMenuItem.expandActionView()
searchView = searchViewMenuItem.actionView as SearchView searchView = searchViewMenuItem.actionView as SearchView
bindSearchView() bindSearchView()
} }
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_filter_search)?.apply {
isVisible = showFilterIcon
}
return super<BottomSheetActivity>.onPrepareMenu(menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
super.onMenuItemSelected(menuItem) return when (menuItem.itemId) {
return false R.id.action_filter_search -> {
binding.chipsFilter.toggleVisibility()
binding.chipsFilter2.toggleVisibility()
binding.chipsFilter3.toggleVisibility()
true
}
else -> super.onMenuItemSelected(menuItem)
}
} }
private fun getPageTitle(position: Int): CharSequence { private fun getPageTitle(position: Int): CharSequence {
@ -965,6 +1018,27 @@ class SearchActivity :
searchView.setIconifiedByDefault(false) searchView.setIconifiedByDefault(false)
searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName)) searchView.setSearchableInfo((getSystemService(Context.SEARCH_SERVICE) as? SearchManager)?.getSearchableInfo(componentName))
setSearchViewWidth(showFilterIcon)
// Keep text that was entered also when switching to a different tab (before the search is executed)
searchView.setOnQueryTextListener(this)
searchView.setQuery(viewModel.currentSearchFieldContent ?: "", false)
// Only focus if the query is empty. This ensures that if the user is returning
// to the search results after visiting a result the full list is available,
// instead of being obscured by the keyboard.
if (viewModel.currentQuery.isBlank()) searchView.requestFocus()
}
/**
* Compute and set the width of [searchView].
*
* @param showingFilterIcon True if the filter icon is showing and the width should
* be adjusted to account for this.
*/
private fun setSearchViewWidth(showingFilterIcon: Boolean) {
if (!this::searchView.isInitialized) return
// SearchView has a bug. If it's displayed 'app:showAsAction="always"' it's too wide, // SearchView has a bug. If it's displayed 'app:showAsAction="always"' it's too wide,
// pushing other icons (including the options menu '...' icon) off the edge of the // pushing other icons (including the options menu '...' icon) off the edge of the
// screen. // screen.
@ -986,20 +1060,11 @@ class SearchActivity :
// It appears to be impossible to override this behaviour on API level < 33. // It appears to be impossible to override this behaviour on API level < 33.
// //
// SearchView does allow you to specify the maximum width. So take the screen width, // SearchView does allow you to specify the maximum width. So take the screen width,
// subtract 48dp * 2 (for the menu icon and back icon on either side), convert to pixels, // subtract 48dp * iconCount (for the menu, filter, and back icons), convert to pixels, and use that.
// and use that. val iconCount = if (showingFilterIcon) 3 else 2
val pxScreenWidth = resources.displayMetrics.widthPixels val pxScreenWidth = resources.displayMetrics.widthPixels
val pxBuffer = ((48 * 2) * resources.displayMetrics.density).toInt() val pxBuffer = ((48 * iconCount) * resources.displayMetrics.density).toInt()
searchView.maxWidth = pxScreenWidth - pxBuffer searchView.maxWidth = pxScreenWidth - pxBuffer
// Keep text that was entered also when switching to a different tab (before the search is executed)
searchView.setOnQueryTextListener(this)
searchView.setQuery(viewModel.currentSearchFieldContent ?: "", false)
// Only focus if the query is empty. This ensures that if the user is returning
// to the search results after visiting a result the full list is available,
// instead of being obscured by the keyboard.
if (viewModel.currentQuery.isBlank()) searchView.requestFocus()
} }
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {

View File

@ -36,6 +36,20 @@ import app.pachli.components.search.adapter.SearchPagingSourceFactory
import app.pachli.core.accounts.AccountManager import app.pachli.core.accounts.AccountManager
import app.pachli.core.data.repository.ServerRepository import app.pachli.core.data.repository.ServerRepository
import app.pachli.core.database.model.AccountEntity import app.pachli.core.database.model.AccountEntity
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_BY_DATE
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_FROM
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_HAS_AUDIO
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_HAS_EMBED
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_HAS_IMAGE
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_HAS_LINK
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_HAS_MEDIA
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_HAS_POLL
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_HAS_VIDEO
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IN_LIBRARY
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IN_PUBLIC
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_REPLY
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE
import app.pachli.core.network.ServerOperation.ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE
import app.pachli.core.network.model.DeletedStatus import app.pachli.core.network.model.DeletedStatus
import app.pachli.core.network.model.Poll import app.pachli.core.network.model.Poll
import app.pachli.core.network.model.Status import app.pachli.core.network.model.Status
@ -47,7 +61,9 @@ import app.pachli.viewdata.StatusViewData
import at.connyduck.calladapter.networkresult.NetworkResult import at.connyduck.calladapter.networkresult.NetworkResult
import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.fold
import at.connyduck.calladapter.networkresult.onFailure import at.connyduck.calladapter.networkresult.onFailure
import com.github.michaelbull.result.mapBoth
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.z4kn4fein.semver.constraints.toConstraint
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -118,6 +134,68 @@ class SearchViewModel @Inject constructor(
null, null,
) )
/**
* Set of operators the server supports.
*
* Empty set if the server does not support any operators.
*/
val availableOperators = serverRepository.flow.map { result ->
result.mapBoth(
{ server ->
buildSet {
val constraint100 = ">=1.0.0".toConstraint()
val canHasMedia = server.can(ORG_JOINMASTODON_SEARCH_QUERY_HAS_MEDIA, constraint100)
val canHasImage = server.can(ORG_JOINMASTODON_SEARCH_QUERY_HAS_IMAGE, constraint100)
val canHasVideo = server.can(ORG_JOINMASTODON_SEARCH_QUERY_HAS_VIDEO, constraint100)
val canHasAudio = server.can(ORG_JOINMASTODON_SEARCH_QUERY_HAS_AUDIO, constraint100)
if (canHasMedia || canHasImage || canHasVideo || canHasAudio) {
add(HasMediaOperator())
}
if (server.can(ORG_JOINMASTODON_SEARCH_QUERY_BY_DATE, constraint100)) {
add(DateOperator())
}
if (server.can(ORG_JOINMASTODON_SEARCH_QUERY_FROM, constraint100)) {
add(FromOperator())
}
if (server.can(ORG_JOINMASTODON_SEARCH_QUERY_LANGUAGE, constraint100)) {
add(LanguageOperator())
}
if (server.can(ORG_JOINMASTODON_SEARCH_QUERY_HAS_LINK, constraint100)) {
add(HasLinkOperator())
}
if (server.can(ORG_JOINMASTODON_SEARCH_QUERY_HAS_EMBED, constraint100)) {
add(HasEmbedOperator())
}
if (server.can(ORG_JOINMASTODON_SEARCH_QUERY_HAS_POLL, constraint100)) {
add(HasPollOperator())
}
if (server.can(ORG_JOINMASTODON_SEARCH_QUERY_IS_REPLY, constraint100)) {
add(IsReplyOperator())
}
if (server.can(ORG_JOINMASTODON_SEARCH_QUERY_IS_SENSITIVE, constraint100)) {
add(IsSensitiveOperator())
}
val canInLibrary = server.can(ORG_JOINMASTODON_SEARCH_QUERY_IN_LIBRARY, constraint100)
val canInPublic = server.can(ORG_JOINMASTODON_SEARCH_QUERY_IN_PUBLIC, constraint100)
if (canInLibrary || canInPublic) add(WhereOperator())
}
},
{
emptySet()
},
)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
emptySet(),
)
private val loadedStatuses: MutableList<StatusViewData> = mutableListOf() private val loadedStatuses: MutableList<StatusViewData> = mutableListOf()
private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) { private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) {
@ -162,7 +240,7 @@ class SearchViewModel @Inject constructor(
* with [viewData]. * with [viewData].
*/ */
fun <T : SearchOperator> replaceOperator(viewData: SearchOperatorViewData<T>) = _operatorViewData.update { operators -> fun <T : SearchOperator> replaceOperator(viewData: SearchOperatorViewData<T>) = _operatorViewData.update { operators ->
operators.find { it.javaClass == viewData.javaClass }?.let { operators - it + viewData } ?: (operators + viewData) operators.find { it.javaClass == viewData.javaClass }?.let { operators - it + viewData } ?: operators
} }
fun search() { fun search() {

View File

@ -21,100 +21,141 @@
app:layout_scrollFlags="scroll|snap|enterAlways" app:layout_scrollFlags="scroll|snap|enterAlways"
app:navigationIcon="?attr/homeAsUpIndicator" /> app:navigationIcon="?attr/homeAsUpIndicator" />
<com.google.android.material.chip.ChipGroup <HorizontalScrollView
android:id="@+id/chipsFilter" android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
app:layout_scrollFlags="scroll|snap|enterAlways" app:layout_scrollFlags="scroll|snap|enterAlways"
android:animateLayoutChanges="true"> android:scrollbars="none">
<com.google.android.material.chip.Chip <com.google.android.material.chip.ChipGroup
android:id="@+id/chipFrom" android:id="@+id/chipsFilter"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:chipIconEnabled="true" android:paddingStart="?listPreferredItemPaddingStart"
android:text="@string/search_operator_from_all" /> android:paddingEnd="?listPreferredItemPaddingEnd"
app:singleLine="true"
android:animateLayoutChanges="true">
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chipDate" android:id="@+id/chipFrom"
style="@style/Widget.Material3.Chip.Suggestion" style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIconEnabled="true"
android:text="@string/search_operator_from_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chipDate"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIconEnabled="true"
android:text="@string/search_operator_date_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chipLanguage"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIconEnabled="true"
android:text="@string/search_operator_language_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chipWhere"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIconEnabled="true"
android:text="@string/search_operator_where_all" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|snap|enterAlways"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipsFilter2"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:chipIconEnabled="true" android:paddingStart="?listPreferredItemPaddingStart"
android:text="@string/search_operator_date_all" /> android:paddingEnd="?listPreferredItemPaddingEnd"
app:singleLine="true"
android:animateLayoutChanges="true">
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chipLanguage" android:id="@+id/chip_has_media"
style="@style/Widget.Material3.Chip.Suggestion" style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_attach_file_24dp"
app:chipIconEnabled="true"
android:text="@string/search_operator_attachment_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_has_poll"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_attach_file_24dp"
app:chipIconEnabled="true"
android:text="@string/search_operator_poll_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_has_link"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_link_24"
app:chipIconEnabled="true"
android:text="@string/search_operator_link_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_has_embed"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIconEnabled="true"
android:text="@string/search_operator_embed_all" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|snap|enterAlways"
android:scrollbars="none">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipsFilter3"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:chipIconEnabled="true" android:paddingStart="?listPreferredItemPaddingStart"
android:text="@string/search_operator_language_all" /> android:paddingEnd="?listPreferredItemPaddingEnd"
app:singleLine="true"
android:animateLayoutChanges="true">
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chip_has_media" android:id="@+id/chip_is_reply"
style="@style/Widget.Material3.Chip.Suggestion" style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_attach_file_24dp" app:chipIcon="@drawable/ic_reply_all_24dp"
app:chipIconEnabled="true" app:chipIconEnabled="true"
android:text="@string/search_operator_attachment_all" /> android:text="@string/search_operator_replies_all" />
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/chip_is_reply" android:id="@+id/chip_is_sensitive"
style="@style/Widget.Material3.Chip.Suggestion" style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_reply_all_24dp" app:chipIcon="@drawable/ic_eye_24dp"
app:chipIconEnabled="true" app:chipIconEnabled="true"
android:text="@string/search_operator_replies_all" /> android:text="@string/search_operator_sensitive_all" />
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.chip.Chip </HorizontalScrollView>
android:id="@+id/chip_is_sensitive"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_eye_24dp"
app:chipIconEnabled="true"
android:text="@string/search_operator_sensitive_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_has_poll"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_attach_file_24dp"
app:chipIconEnabled="true"
android:text="@string/search_operator_poll_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_has_embed"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIconEnabled="true"
android:text="@string/search_operator_embed_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chip_has_link"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIcon="@drawable/ic_link_24"
app:chipIconEnabled="true"
android:text="@string/search_operator_link_all" />
<com.google.android.material.chip.Chip
android:id="@+id/chipWhere"
style="@style/Widget.Material3.Chip.Suggestion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipIconEnabled="true"
android:text="@string/search_operator_where_all" />
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabs" android:id="@+id/tabs"

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
@ -8,5 +9,12 @@
android:icon="@android:drawable/ic_menu_search" android:icon="@android:drawable/ic_menu_search"
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
android:actionLayout="@layout/search_view" android:actionLayout="@layout/search_view"
app:showAsAction="always" /> app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/action_filter_search"
android:title="@string/action_filter_search"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
</menu> </menu>

View File

@ -165,6 +165,7 @@
<string name="action_accept">Accept</string> <string name="action_accept">Accept</string>
<string name="action_reject">Reject</string> <string name="action_reject">Reject</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_filter_search">Filter search</string>
<string name="action_access_drafts">Drafts</string> <string name="action_access_drafts">Drafts</string>
<string name="action_access_scheduled_posts">Scheduled posts</string> <string name="action_access_scheduled_posts">Scheduled posts</string>
<string name="action_toggle_visibility">Post visibility</string> <string name="action_toggle_visibility">Post visibility</string>

View File

@ -30,3 +30,11 @@ fun View.hide() {
fun View.visible(visible: Boolean, or: Int = View.GONE) { fun View.visible(visible: Boolean, or: Int = View.GONE) {
this.visibility = if (visible) View.VISIBLE else or this.visibility = if (visible) View.VISIBLE else or
} }
fun View.toggleVisibility() {
when (this.visibility) {
View.GONE -> this.show()
View.INVISIBLE -> this.show()
View.VISIBLE -> this.hide()
}
}

View File

@ -8,7 +8,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/styles.xml" file="src/main/res/values/styles.xml"
line="134" line="137"
column="42"/> column="42"/>
</issue> </issue>
@ -19,7 +19,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location <location
file="src/main/res/values/styles.xml" file="src/main/res/values/styles.xml"
line="135" line="138"
column="43"/> column="43"/>
</issue> </issue>