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
Expand Up @@ -107,6 +107,7 @@ class LayoutValidationTest : InstrumentedTest() {
com.ichi2.anki.R.layout.reviewer2,
com.ichi2.anki.R.layout.preferences,
com.ichi2.anki.R.layout.drawing_fragment,
com.ichi2.anki.R.layout.card_browser_searchview_fragment,
) +
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
listOf(com.ichi2.anki.R.layout.widget_small_unthemed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.TextView
import androidx.annotation.LayoutRes
Expand All @@ -33,7 +34,9 @@ import androidx.core.content.ContextCompat
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
Expand Down Expand Up @@ -69,6 +72,8 @@ import com.ichi2.anki.browser.CardBrowserViewModel.ToggleSelectionState.SELECT_N
import com.ichi2.anki.browser.RepositionCardFragment.Companion.REQUEST_REPOSITION_NEW_CARDS
import com.ichi2.anki.browser.RepositionCardsRequest.ContainsNonNewCardsError
import com.ichi2.anki.browser.RepositionCardsRequest.RepositionData
import com.ichi2.anki.browser.search.AdvancedSearchFragment
import com.ichi2.anki.browser.search.StandardSearchFragment
import com.ichi2.anki.common.annotations.NeedsTest
import com.ichi2.anki.common.utils.android.isRobolectric
import com.ichi2.anki.common.utils.annotation.KotlinCleanup
Expand Down Expand Up @@ -105,6 +110,7 @@ import com.ichi2.anki.utils.showDialogFragmentImpl
import com.ichi2.anki.withProgress
import com.ichi2.utils.HandlerUtils
import com.ichi2.utils.TagsUtil.getUpdatedTags
import com.ichi2.utils.replaceText
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -160,6 +166,8 @@ class CardBrowserFragment :
private var searchView: SearchView? = null
private var deckChip: Chip? = null

private var toggleAdvancedSearch: Button? = null

@get:LayoutRes
private val layout: Int
get() = if (useSearchView) R.layout.card_browser_searchview_fragment else R.layout.card_browser_fragment
Expand Down Expand Up @@ -229,7 +237,32 @@ class CardBrowserFragment :
requireNavigationDrawerActivity().onNavigationPressed()
}
}
searchView = view.findViewById<SearchView>(R.id.search_view)

fun FragmentTransaction.removeByTag(tag: String): FragmentTransaction {
val fragment = childFragmentManager.findFragmentByTag(tag) ?: return this
return remove(fragment)
}

searchView =
view.findViewById<SearchView>(R.id.search_view)?.apply {
editText.doAfterTextChanged { viewModel.onSearchTextChanged(it.toString()) }
addTransitionListener { _, _, state ->
if (state != SearchView.TransitionState.HIDDEN) return@addTransitionListener
// clear state on hide
childFragmentManager
.beginTransaction()
.removeByTag(FRAGMENT_TAG_STANDARD)
.removeByTag(FRAGMENT_TAG_ADVANCED)
.commit()
viewModel.clearTemporarySearchState()
}
}
toggleAdvancedSearch =
view.findViewById<Button>(R.id.toggle_advanced_search)?.apply {
setOnClickListener {
viewModel.toggleAdvancedSearch()
}
}

setupFlows()

Expand Down Expand Up @@ -454,6 +487,37 @@ class CardBrowserFragment :
deckChip?.text = deck?.getFullDisplayName(requireContext())
}

fun advancedSearchChanged(inAdvancedSearch: Boolean) {
toggleAdvancedSearch?.text = if (inAdvancedSearch) "Basic search" else "Advanced search"

if (searchView == null) return

// switch to the correct fragment, retaining state
fun findOrCreate(
tag: String,
factory: () -> Fragment,
): Fragment =
childFragmentManager.findFragmentByTag(tag) ?: factory().also {
childFragmentManager
.beginTransaction()
.add(R.id.search_view_content_container, it, tag)
.commit()
}

val standard = findOrCreate(FRAGMENT_TAG_STANDARD) { StandardSearchFragment() }
val advanced = findOrCreate(FRAGMENT_TAG_ADVANCED) { AdvancedSearchFragment() }

childFragmentManager
.beginTransaction()
.hide(if (inAdvancedSearch) standard else advanced)
.show(if (inAdvancedSearch) advanced else standard)
.commit()
}

fun onTemporaryTextChanged(text: String) {
searchView?.editText?.replaceText(text)
}

activityViewModel.flowOfIsTruncated.launchCollectionInLifecycleScope(::onIsTruncatedChanged)
activityViewModel.flowOfSelectedRows.launchCollectionInLifecycleScope(::onSelectedRowsChanged)
activityViewModel.flowOfActiveColumns.launchCollectionInLifecycleScope(::onColumnsChanged)
Expand All @@ -466,6 +530,8 @@ class CardBrowserFragment :
viewModel.flowOfSearchForDecks.launchCollectionInLifecycleScope(::onSearchForDecks)
activityViewModel.flowOfDeckSelection.launchCollectionInLifecycleScope(::onDeckChanged)
activityViewModel.flowOfScrollRequest.launchCollectionInLifecycleScope(::autoScrollTo)
viewModel.advancedSearchFlow.launchCollectionInLifecycleScope(::advancedSearchChanged)
viewModel.searchTextFlow.launchCollectionInLifecycleScope(::onTemporaryTextChanged)
}

private fun setupFragmentResultListeners() {
Expand Down Expand Up @@ -1058,5 +1124,8 @@ class CardBrowserFragment :
* since the cards are unselected when this happens
*/
private const val CHANGE_DECK_KEY = "CHANGE_DECK"

const val FRAGMENT_TAG_STANDARD = "STANDARD"
const val FRAGMENT_TAG_ADVANCED = "ADVANCED"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,92 @@

package com.ichi2.anki.browser

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.ichi2.anki.common.annotations.NeedsTest
import com.ichi2.anki.model.SelectableDeck
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import timber.log.Timber

class CardBrowserFragmentViewModel : ViewModel() {
class CardBrowserFragmentViewModel(
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
val flowOfSearchForDecks = MutableSharedFlow<List<SelectableDeck>>()

val advancedSearchFlow =
savedStateHandle.getMutableStateFlow(STATE_ADVANCED_SEARCH_ENABLED, false)

private val advancedSearchTextFlow =
savedStateHandle.getMutableStateFlow(STATE_ADVANCED_SEARCH_TEXT, "")
private val basicSearchTextFlow =
savedStateHandle.getMutableStateFlow(STATE_BASIC_SEARCH_TEXT, "")

val searchTextFlow =
combine(advancedSearchFlow, basicSearchTextFlow, advancedSearchTextFlow) { displayingAdvancedSearch, basicText, advancedText ->
if (displayingAdvancedSearch) advancedText else basicText
}

@NeedsTest("default usage")
fun openDeckSelectionDialog() =
viewModelScope.launch {
val decks = listOf(SelectableDeck.AllDecks) + SelectableDeck.fromCollection(includeFiltered = true)
flowOfSearchForDecks.emit(decks)
}

/**
* Toggles between Basic and Advanced search mode
*/
fun toggleAdvancedSearch() {
Timber.i("Toggling advanced search to %s", !advancedSearchFlow.value)
advancedSearchFlow.value = !advancedSearchFlow.value
}

/**
* Appends [searchText] to the current temporary advances search text
*/
fun appendAdvancedSearch(searchText: String) {
Timber.d("appending search text '%s'", searchText)
advancedSearchTextFlow.value +=
buildString {
if (!advancedSearchTextFlow.value.endsWith(" ")) append(' ')
append(searchText)
append(' ')
}
}

/**
* Called on user modification to the temporary search text
*/
fun onSearchTextChanged(searchText: String) {
if (advancedSearchFlow.value) {
advancedSearchTextFlow.value = searchText
} else {
basicSearchTextFlow.value = searchText
}
}

/**
* Clears state when the temporary search is no longer visible
*
* If a user backs out of the SearchView, it should reset
*/
fun clearTemporarySearchState() {
Timber.i("clearing temp search state")
advancedSearchFlow.value = false
basicSearchTextFlow.value = ""
advancedSearchTextFlow.value = ""

savedStateHandle.remove<Any>(STATE_ADVANCED_SEARCH_ENABLED)
savedStateHandle.remove<Any>(STATE_BASIC_SEARCH_TEXT)
savedStateHandle.remove<Any>(STATE_ADVANCED_SEARCH_TEXT)
}

companion object {
private const val STATE_ADVANCED_SEARCH_ENABLED = "advancedSearch"
private const val STATE_BASIC_SEARCH_TEXT = "basicSearchText"
private const val STATE_ADVANCED_SEARCH_TEXT = "advancedSearchText"
}
}
Loading