Skip to content

Commit c1c37f1

Browse files
committed
feat(card-browser): implement 'Default search text'
Fixes 18618 Assisted-by: Claude Opus 4.7 - all
1 parent ac8455d commit c1c37f1

7 files changed

Lines changed: 121 additions & 0 deletions

File tree

AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserOptionsRepository.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.content.SharedPreferences
2020
import androidx.core.content.edit
2121
import com.ichi2.anki.CollectionManager.withCol
2222
import com.ichi2.anki.model.CardsOrNotes
23+
import com.ichi2.anki.utils.ext.defaultBrowserSearch
2324
import com.ichi2.anki.utils.ext.ignoreAccentsInSearch
2425
import kotlinx.coroutines.flow.MutableStateFlow
2526
import kotlinx.coroutines.flow.StateFlow
@@ -40,10 +41,15 @@ class BrowserOptionsRepository(
4041
val ignoreAccentsInSearch: StateFlow<Boolean>
4142
field = MutableStateFlow(false)
4243

44+
/** @see com.ichi2.anki.libanki.Config.defaultBrowserSearch */
45+
val defaultBrowserSearch: StateFlow<String>
46+
field = MutableStateFlow("")
47+
4348
/** Reads persisted values into the flows. Call once during ViewModel init. */
4449
suspend fun load() {
4550
cardsOrNotes.value = withCol { CardsOrNotes.fromCollection(this) }
4651
ignoreAccentsInSearch.value = withCol { config.ignoreAccentsInSearch }
52+
defaultBrowserSearch.value = withCol { config.defaultBrowserSearch }
4753
}
4854

4955
suspend fun setCardsOrNotes(value: CardsOrNotes) {
@@ -67,6 +73,13 @@ class BrowserOptionsRepository(
6773
ignoreAccentsInSearch.value = value
6874
}
6975

76+
suspend fun setDefaultBrowserSearch(value: String) {
77+
if (defaultBrowserSearch.value == value) return
78+
Timber.d("setting default browser search to %s", value)
79+
withCol { config.defaultBrowserSearch = value }
80+
defaultBrowserSearch.value = value
81+
}
82+
7083
companion object {
7184
private const val PREF_IS_TRUNCATED = "isTruncated"
7285
}

AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserFragment.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,11 @@ class CardBrowserFragment :
969969
Timber.d("syncing searchview state from chip updates")
970970
val filters = search.filters
971971

972+
// Handle default search text
973+
if (search.query.isNotEmpty() && searchBar?.text.isNullOrEmpty()) {
974+
launchCatchingTask { searchBar?.setText(search.toUserSpannable()) }
975+
}
976+
972977
decksChip?.text = filters.decks.firstOrNull()?.name ?: getString(R.string.card_browser_all_decks)
973978
decksChip?.hasCheckedBackground = filters.decks.any()
974979

AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ class CardBrowserViewModel(
239239

240240
val shouldIgnoreAccents: Boolean get() = browserOptionsRepository.ignoreAccentsInSearch.value
241241

242+
val defaultBrowserSearch: String get() = browserOptionsRepository.defaultBrowserSearch.value
243+
242244
private val _selectedRows: MutableSet<CardOrNoteId> = Collections.synchronizedSet(LinkedHashSet())
243245

244246
// immutable accessor for _selectedRows
@@ -500,6 +502,11 @@ class CardBrowserViewModel(
500502
viewModelScope.launch {
501503
browserOptionsRepository.load()
502504

505+
// Apply the default search (if available)
506+
if (Prefs.devUsingCardBrowserSearchView) {
507+
searchTerms = searchTerms.ifEmpty { defaultBrowserSearch }
508+
}
509+
503510
val initialDeckId = if (selectAllDecks) SelectableDeck.AllDecks else getInitialDeck()
504511
// PERF: slightly inefficient if the source was lastDeckId
505512
setSelectedDeck(initialDeckId)
@@ -693,6 +700,12 @@ class CardBrowserViewModel(
693700

694701
fun setIgnoreAccents(value: Boolean) = viewModelScope.launch { browserOptionsRepository.setIgnoreAccentsInSearch(value) }
695702

703+
fun setDefaultSearchText(text: String) =
704+
viewModelScope.launch {
705+
if (!Prefs.devUsingCardBrowserSearchView) return@launch
706+
browserOptionsRepository.setDefaultBrowserSearch(text)
707+
}
708+
696709
fun selectAll(): Job? {
697710
if (!_selectedRows.addAll(cards)) return null
698711
Timber.d("selecting all: %d item(s)", cards.size)

AnkiDroid/src/main/java/com/ichi2/anki/dialogs/BrowserOptionsDialog.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,20 @@ package com.ichi2.anki.dialogs
1818

1919
import android.app.Dialog
2020
import android.os.Bundle
21+
import android.text.Editable
2122
import androidx.appcompat.app.AppCompatDialogFragment
2223
import androidx.core.os.bundleOf
24+
import androidx.core.view.isVisible
2325
import androidx.fragment.app.activityViewModels
26+
import anki.config.ConfigKey
2427
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2528
import com.ichi2.anki.CollectionManager.TR
2629
import com.ichi2.anki.R
2730
import com.ichi2.anki.browser.BrowserColumnSelectionFragment
2831
import com.ichi2.anki.browser.CardBrowserViewModel
2932
import com.ichi2.anki.databinding.DialogBrowserOptionsBinding
3033
import com.ichi2.anki.model.CardsOrNotes
34+
import com.ichi2.anki.settings.Prefs
3135
import com.ichi2.utils.create
3236
import com.ichi2.utils.negativeButton
3337
import com.ichi2.utils.positiveButton
@@ -52,6 +56,7 @@ class BrowserOptionsDialog : AppCompatDialogFragment(R.layout.dialog_browser_opt
5256
viewModel.setCardsOrNotes(dialogCardsOrNotes)
5357
viewModel.setTruncated(binding.truncateCheckBox.isChecked)
5458
viewModel.setIgnoreAccents(binding.ignoreAccentsCheckBox.isChecked)
59+
viewModel.setDefaultSearchText(binding.defaultSearchText.text.orEmpty())
5560
}
5661

5762
private val cardsOrNotes by lazy {
@@ -100,6 +105,17 @@ class BrowserOptionsDialog : AppCompatDialogFragment(R.layout.dialog_browser_opt
100105
isChecked = viewModel.shouldIgnoreAccents
101106
}
102107

108+
if (Prefs.devUsingCardBrowserSearchView) {
109+
binding.defaultSearchInputLayout.apply {
110+
isVisible = true
111+
hint = TR.preferencesDefaultSearchText()
112+
}
113+
binding.defaultSearchText.apply {
114+
hint = TR.preferencesDefaultSearchTextExample()
115+
setText(viewModel.defaultBrowserSearch)
116+
}
117+
}
118+
103119
binding.browsingTextView.text = TR.preferencesBrowsing()
104120

105121
return MaterialAlertDialogBuilder(requireContext()).create {
@@ -135,3 +151,6 @@ class BrowserOptionsDialog : AppCompatDialogFragment(R.layout.dialog_browser_opt
135151
}
136152
}
137153
}
154+
155+
/** Returns the text content as a [String], or `""` if the receiver is `null`. */
156+
private fun Editable?.orEmpty(): String = this?.toString().orEmpty()

AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/Config.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,12 @@ import com.ichi2.anki.libanki.Config
3333
var Config.ignoreAccentsInSearch
3434
get() = getBool(ConfigKey.Bool.IGNORE_ACCENTS_IN_SEARCH)
3535
set(value) = setBool(ConfigKey.Bool.IGNORE_ACCENTS_IN_SEARCH, value)
36+
37+
/**
38+
* Default search text for the card browser
39+
*
40+
* e.g. "deck:current"
41+
*/
42+
var Config.defaultBrowserSearch
43+
get() = getString(ConfigKey.String.DEFAULT_SEARCH_TEXT)
44+
set(value) = setString(ConfigKey.String.DEFAULT_SEARCH_TEXT, value)

AnkiDroid/src/main/res/layout/dialog_browser_options.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@
9292
android:layout_height="wrap_content"
9393
android:layout_marginHorizontal="16dp"
9494
tools:text="Ignore accents in search (slower)"/>
95+
96+
<com.google.android.material.textfield.TextInputLayout
97+
android:id="@+id/default_search_input_layout"
98+
android:layout_width="match_parent"
99+
android:layout_height="wrap_content"
100+
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
101+
app:expandedHintEnabled="false"
102+
app:endIconMode="clear_text"
103+
android:layout_marginHorizontal="16dp"
104+
android:layout_marginTop="8dp"
105+
android:visibility="gone"
106+
tools:visibility="visible"
107+
tools:hint="Default search text">
108+
<com.google.android.material.textfield.TextInputEditText
109+
android:id="@+id/default_search_text"
110+
android:layout_width="match_parent"
111+
android:layout_height="wrap_content"
112+
android:inputType="text"
113+
tools:hint="e.g. 'deck:current'"/>
114+
</com.google.android.material.textfield.TextInputLayout>
95115
</LinearLayout>
96116

97117
<LinearLayout

AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import com.ichi2.anki.model.SortType
8282
import com.ichi2.anki.servicelayer.NoteService
8383
import com.ichi2.anki.setFlagFilterSync
8484
import com.ichi2.anki.settings.Prefs
85+
import com.ichi2.anki.utils.ext.defaultBrowserSearch
8586
import com.ichi2.anki.utils.ext.ifNotZero
8687
import com.ichi2.anki.utils.ext.ignoreAccentsInSearch
8788
import com.ichi2.testutils.IntentAssert
@@ -1570,6 +1571,47 @@ class CardBrowserViewModelTest : JvmTest() {
15701571
}
15711572
}
15721573

1574+
@Test
1575+
fun `default search text is applied on init`() {
1576+
col.config.defaultBrowserSearch = "deck:current"
1577+
try {
1578+
Prefs.devUsingCardBrowserSearchView = true
1579+
runViewModelTest {
1580+
assertThat(searchTerms, equalTo("deck:current"))
1581+
assertThat(defaultBrowserSearch, equalTo("deck:current"))
1582+
}
1583+
} finally {
1584+
Prefs.devUsingCardBrowserSearchView = false
1585+
}
1586+
}
1587+
1588+
@Test
1589+
fun `intent search wins over default search text`() {
1590+
col.config.defaultBrowserSearch = "deck:current"
1591+
try {
1592+
Prefs.devUsingCardBrowserSearchView = true
1593+
runViewModelTest(options = DeepLink("tag:foo")) {
1594+
assertThat(searchTerms, equalTo("tag:foo"))
1595+
}
1596+
} finally {
1597+
Prefs.devUsingCardBrowserSearchView = false
1598+
}
1599+
}
1600+
1601+
@Test
1602+
fun `setDefaultSearchText round-trips through collection config`() {
1603+
try {
1604+
Prefs.devUsingCardBrowserSearchView = true
1605+
runViewModelTest {
1606+
setDefaultSearchText("tag:foo").join()
1607+
assertThat(defaultBrowserSearch, equalTo("tag:foo"))
1608+
assertThat(col.config.defaultBrowserSearch, equalTo("tag:foo"))
1609+
}
1610+
} finally {
1611+
Prefs.devUsingCardBrowserSearchView = false
1612+
}
1613+
}
1614+
15731615
@Test
15741616
fun `updating sort type launches search`() =
15751617
runViewModelTest {

0 commit comments

Comments
 (0)