Repository navigation
feat(export): pick the deck with the deck selection dialog #22678
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
Merged
criticalAY
merged 2 commits into
ankidroid:main
from
criticalAY:feat/export-deck-selection
Nov 3, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,18 +12,19 @@ import android.view.WindowManager | |
| import android.widget.Filter | ||
| import android.widget.Filterable | ||
| import android.widget.ImageButton | ||
| import androidx.annotation.VisibleForTesting | ||
| import androidx.appcompat.app.AlertDialog | ||
| import androidx.appcompat.widget.SearchView | ||
| import androidx.appcompat.widget.Toolbar | ||
| import androidx.core.content.res.getDrawableOrThrow | ||
| import androidx.core.content.res.use | ||
| import androidx.core.os.BundleCompat | ||
| import androidx.core.view.isVisible | ||
| import androidx.fragment.app.Fragment | ||
| import androidx.fragment.app.FragmentActivity | ||
| import androidx.fragment.app.setFragmentResultListener | ||
| import androidx.recyclerview.widget.DividerItemDecoration | ||
| import androidx.recyclerview.widget.RecyclerView | ||
| import anki.collection.opChanges | ||
| import anki.decks.deckTreeNode | ||
| import com.ichi2.anki.CardTemplateEditor | ||
| import com.ichi2.anki.CollectionManager.TR | ||
|
|
@@ -44,8 +45,10 @@ import com.ichi2.anki.libanki.Consts.DEFAULT_DECK_ID | |
| import com.ichi2.anki.libanki.DeckId | ||
| import com.ichi2.anki.libanki.sched.DeckNode | ||
| import com.ichi2.anki.model.SelectableDeck | ||
| import com.ichi2.anki.observability.ChangeManager | ||
| import com.ichi2.anki.ui.internationalization.sentenceCase | ||
| import com.ichi2.anki.utils.ext.getParcelableCompat | ||
| import com.ichi2.anki.utils.ext.requireLongArray | ||
| import com.ichi2.anki.utils.ext.setFragmentResultListener | ||
| import com.ichi2.anki.withProgress | ||
| import com.ichi2.ui.AccessibleSearchView | ||
|
|
@@ -54,6 +57,7 @@ import com.ichi2.utils.create | |
| import com.ichi2.utils.customView | ||
| import com.ichi2.utils.negativeButton | ||
| import com.ichi2.utils.positiveButton | ||
| import kotlinx.coroutines.Job | ||
| import timber.log.Timber | ||
| import com.ichi2.anki.common.android.R as CommonR | ||
|
|
||
|
|
@@ -68,7 +72,9 @@ import com.ichi2.anki.common.android.R as CommonR | |
| @NeedsTest("test the ordering of decks in search page in the dialog") | ||
| @NeedsTest("test syncing the status of collapsing deck with teh deckPicker") | ||
| class DeckSelectionDialog : AnalyticsDialogFragment() { | ||
| private lateinit var binding: DialogDeckPickerBinding | ||
| @VisibleForTesting | ||
| internal lateinit var binding: DialogDeckPickerBinding | ||
| private set | ||
| private lateinit var decksAdapter: DecksArrayAdapter | ||
| private lateinit var decksRoot: DeckNode | ||
| private val title: String | ||
|
|
@@ -89,8 +95,8 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| val dividerItemDecoration = | ||
| DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL) | ||
| binding.decks.addItemDecoration(dividerItemDecoration) | ||
| val decks: List<SelectableDeck> = getDeckNames() | ||
| decksAdapter = DecksArrayAdapter(decks) | ||
| val deckIds = getDeckIds() | ||
| decksAdapter = DecksArrayAdapter(deckIds) | ||
| binding.decks.adapter = decksAdapter | ||
| setupMenu() | ||
| return AlertDialog.Builder(requireActivity()).create { | ||
|
|
@@ -112,8 +118,7 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| ) | ||
| } | ||
|
|
||
| private fun getDeckNames(): ArrayList<SelectableDeck> = | ||
| BundleCompat.getParcelableArrayList(requireArguments(), ARG_DECK_NAMES, SelectableDeck::class.java)!! | ||
| private fun getDeckIds(): LongArray = requireArguments().requireLongArray(ARG_DECK_IDS) | ||
|
|
||
| private fun setupMenu() { | ||
| val toolbar: Toolbar = binding.toolbar | ||
|
|
@@ -175,6 +180,13 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| /** Updates the list and simulates a click on the newly created deck */ | ||
| private fun onNewDeckCreated(id: DeckId) { | ||
| // a deck/subdeck was created | ||
| ChangeManager.notifySubscribers( | ||
| opChanges { | ||
| deck = true | ||
| studyQueues = true | ||
| }, | ||
| initiator = this, | ||
| ) | ||
| launchCatchingTask { | ||
| val name = withCol { decks.name(id) } | ||
| val deck = SelectableDeck.Deck(id, name) | ||
|
|
@@ -205,7 +217,7 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| decksAdapter.removeDeck(deck.deckId) | ||
| } | ||
| // dismiss dialog when all decks have been selected | ||
| if (decksAdapter.itemCount == 0) { | ||
| if (decksAdapter.remainingDeckCount == 0) { | ||
| dialog!!.dismiss() | ||
| } | ||
| } else { | ||
|
|
@@ -214,7 +226,7 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| } | ||
|
|
||
| open inner class DecksArrayAdapter( | ||
| decks: List<SelectableDeck>, | ||
| deckIds: LongArray, | ||
| ) : RecyclerView.Adapter<DecksArrayAdapter.ViewHolder>(), | ||
| Filterable { | ||
| private lateinit var expandImage: Drawable | ||
|
|
@@ -274,19 +286,29 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
|
|
||
| private fun updateCurrentlyDisplayedDecks() { | ||
| currentlyDisplayedDecks.clear() | ||
| currentlyDisplayedDecks.addAll(allDecksList.filter(::isViewable)) | ||
| currentlyDisplayedDecks.addAll(allDecksList.filter { currentFilters.accept(it.fullDeckName) && isViewable(it) }) | ||
| notifyDataSetChanged() | ||
| } | ||
|
|
||
| private fun showSearchResults() { | ||
| // Expand the matching decks, so matching subdecks under them are listed too | ||
| allDecksList.filter { currentFilters.accept(it.fullDeckName) }.forEach { it.collapsed = false } | ||
| updateCurrentlyDisplayedDecks() | ||
| } | ||
|
|
||
| fun removeDeck(deckId: DeckId) { | ||
| val idsToRemove = mutableSetOf(deckId) | ||
| decksRoot.find(deckId)?.forEach { idsToRemove.add(it.did) } | ||
| allDecksList.removeAll { it.did in idsToRemove } | ||
| updateCurrentlyDisplayedDecks() | ||
| } | ||
|
|
||
| val remainingDeckCount: Int | ||
| get() = allDecksList.size | ||
|
|
||
| private val allDecksList = ArrayList<DeckNode>() | ||
| private val currentlyDisplayedDecks = ArrayList<DeckNode>() | ||
| private var currentFilters = DeckFilters.create("") | ||
|
Comment on lines
+306
to
+311
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No action wanted: Unusual to have these in the middle... but it's the Deck Selection Dialog 🤷 |
||
|
|
||
| override fun onCreateViewHolder( | ||
| parent: ViewGroup, | ||
|
|
@@ -345,7 +367,8 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| val parentNodeRef = deck.parent ?: return true | ||
| // The parent belongs to the tree retained by [allDecksList], so should still exist. | ||
| val parentNode = parentNodeRef.get()!! | ||
| return !parentNode.collapsed && isViewable(parentNode) | ||
| val hidesSubdecks = parentNode.collapsed && currentFilters.accept(parentNode.fullDeckName) | ||
| return !hidesSubdecks && isViewable(parentNode) | ||
| } | ||
|
|
||
| override fun getItemCount(): Int = currentlyDisplayedDecks.size | ||
|
|
@@ -370,12 +393,8 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| constraint: CharSequence?, | ||
| results: List<DeckNode>, | ||
| ) { | ||
| results.forEach { it.collapsed = false } | ||
| currentlyDisplayedDecks.apply { | ||
| clear() | ||
| addAll(results) | ||
| } | ||
| notifyDataSetChanged() | ||
| currentFilters = DeckFilters.create(constraint ?: "") | ||
| showSearchResults() | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -390,11 +409,11 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| launchCatchingTask { | ||
| decksRoot = withCol { Pair(sched.deckDueTree(), isEmpty) }.first | ||
| val allDecksSet = | ||
| decks | ||
| .mapNotNull { it as? SelectableDeck.Deck } | ||
| .mapNotNull { decksRoot.find(it.deckId) } | ||
| deckIds | ||
| .filter { it != ALL_DECKS_ID } | ||
| .mapNotNull { decksRoot.find(it) } | ||
| .toSet() | ||
| if (decks.any { it is SelectableDeck.AllDecks }) { | ||
| if (ALL_DECKS_ID in deckIds) { | ||
| val newDeckNode = | ||
| deckTreeNode { | ||
| deckId = ALL_DECKS_ID | ||
|
|
@@ -404,7 +423,11 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| } | ||
|
|
||
| allDecksList.addAll(allDecksSet) | ||
| updateCurrentlyDisplayedDecks() | ||
| if (currentFilters.isActive()) { | ||
| showSearchResults() | ||
| } else { | ||
| updateCurrentlyDisplayedDecks() | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -421,7 +444,7 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| const val ARG_SKIP_EMPTY_DEFAULT = "arg_skip_empty_default" | ||
| private const val ARG_TITLE = "arg_title" | ||
| private const val ARG_TEMPLATE_EDITOR_MESSAGE = "arg_template_editor_message" | ||
| private const val ARG_DECK_NAMES = "deckNames" | ||
| private const val ARG_DECK_IDS = "arg_deck_ids" | ||
| private const val ARG_ALLOW_MULTIPLE_SELECTION = "arg_allow_multiple_selection" | ||
|
|
||
| /** Creates a new instance of [DeckSelectionDialog]. */ | ||
|
|
@@ -440,7 +463,7 @@ class DeckSelectionDialog : AnalyticsDialogFragment() { | |
| Bundle().apply { | ||
| putString(ARG_TEMPLATE_EDITOR_MESSAGE, templateEditorMessage) | ||
| putString(ARG_TITLE, title) | ||
| putParcelableArrayList(ARG_DECK_NAMES, ArrayList(decks)) | ||
| putLongArray(ARG_DECK_IDS, decks.map { (it as? SelectableDeck.Deck)?.deckId ?: ALL_DECKS_ID }.toLongArray()) | ||
| putString(ARG_REQUEST_KEY, requestKey) | ||
| putBoolean(ARG_ALLOW_MULTIPLE_SELECTION, allowMultipleSelection) | ||
| putBoolean(ARG_ALLOW_ALL, allowAll) | ||
|
|
@@ -484,6 +507,16 @@ fun Fragment.registerDeckSelectedHandler( | |
| } | ||
| } | ||
|
|
||
| fun Fragment.registerChildDeckSelectedHandler( | ||
| requestKey: String = REQUEST_SELECT_DECK, | ||
| action: (deck: SelectableDeck?) -> Unit, | ||
| ) { | ||
| childFragmentManager.setFragmentResultListener(requestKey, this) { _, bundle -> | ||
| val selectedDeck = bundle.getParcelableCompat<SelectableDeck?>(RESULT_SELECTED_DECK) | ||
| action(selectedDeck) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Displays a [DeckSelectionDialog] for the user to select a deck. Using the default parameters will | ||
| * result in all possible decks('All Decks', normal, filtered and the 'Default' deck) being shown. | ||
|
|
@@ -508,8 +541,8 @@ fun Fragment.startDeckSelection( | |
| allowAll: Boolean = true, | ||
| allowFiltered: Boolean = true, | ||
| skipEmptyDefault: Boolean = false, | ||
| ) { | ||
| requireActivity().launchCatchingTask { | ||
| ): Job = | ||
| launchCatchingTask { | ||
| val decks = | ||
| withProgress { | ||
| val backendDecks = | ||
|
|
@@ -543,7 +576,6 @@ fun Fragment.startDeckSelection( | |
| dialog.show(targetManager, DeckSelectionDialog.TAG) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Displays a [DeckSelectionDialog] for the user to select a deck. Using the default parameters will | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd comment this; took a second to understand