Skip to content

feat(export): pick the deck with the deck selection dialog - #22678

Merged
criticalAY merged 2 commits into
ankidroid:mainfrom
criticalAY:feat/export-deck-selection
Nov 3, 2026
Merged

criticalAY merged 2 commits into
ankidroid:mainfrom
criticalAY:feat/export-deck-selection

Conversation

@criticalAY

Copy link
Copy Markdown
Contributor

Note

Assisted-by: Opus 5.5

Purpose / Description

Exporting a deck package or plain text listed every deck in one long drop-down, which gets hard to use once you have a lot of subdecks.

Tapping the deck field now opens the same deck dialog as the Browser and Statistics, with search and collapsible subdecks. "All decks" is still first, filtered decks are still left out, and the chosen deck is kept on rotation. One small difference: an empty "Default" deck is no longer offered, the same as in the deck list.

Fixes

Approach

Check the commit message

How Has This Been Tested?

added tests and

Before After
before-dropdown after-2-deck-dialog

Learning (optional, can help others)

NA

Checklist

Please, go through these checks before submitting the PR.

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

david-allison

This comment was marked as resolved.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Snapshot diff report vs main. Open screenshot-diff for diffs.

  • ExportDialogFragmentScreenshotTest: 1 change
All 1 changed screenshots

ExportDialogFragmentScreenshotTest

  • deck_compare.png

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Oct 21, 2026
@criticalAY
criticalAY force-pushed the feat/export-deck-selection branch from f500927 to 4d9359b Compare October 21, 2026 21:11
@criticalAY criticalAY removed the Needs Author Reply Waiting for a reply from the original author label Oct 21, 2026
david-allison

This comment was marked as resolved.

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Oct 21, 2026
@criticalAY
criticalAY force-pushed the feat/export-deck-selection branch from 4d9359b to 205f6a1 Compare October 22, 2026 07:04
@criticalAY criticalAY removed the Needs Author Reply Waiting for a reply from the original author label Oct 22, 2026
david-allison

This comment was marked as resolved.

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Oct 23, 2026
@criticalAY
criticalAY force-pushed the feat/export-deck-selection branch from 205f6a1 to ee426a0 Compare October 23, 2026 22:53
@criticalAY criticalAY removed the Needs Author Reply Waiting for a reply from the original author label Oct 23, 2026

@david-allison david-allison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, two nits, fix at your discretion!

@david-allison david-allison added Needs Author Reply Waiting for a reply from the original author Needs Second Approval Has one approval, one more approval to merge and removed Needs Review Needs Author Reply Waiting for a reply from the original author labels Oct 24, 2026
@criticalAY
criticalAY force-pushed the feat/export-deck-selection branch from ee426a0 to 4fbcbb5 Compare October 25, 2026 20:35

@mikehardy mikehardy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assisted-by: composer-2.5

Thanks for tightening the export dialog — the direction looks good.

Locally I checked out the PR, rebased cleanly onto current main (no push), ran ./gradlew lintAll ktlintCheck lint-rules:test (pass), ./gradlew jacocoUnitTestReport (fail), and TEST_RELEASE_BUILD=true python3 .github/run_emulator_tests.py on the assigned API 35 play release emulator (382 tests; 11 connected-test XML failures after waivers).

The diff centers on ExportDialogFragment, dialog_export_options.xml, related deck-selection wiring, and new/updated export dialog unit and screenshot tests.

Requesting changes: connected tests on this branch fail in ways that line up with the export UI work — ReviewerTest (two cases) time out waiting for Reviewer and instead resume on DeckPicker, and ReviewRemindersTest fails in the connected run. Please fix those regressions (or adjust navigation/test setup so reviewer flows still reach Reviewer) and re-run the emulator suite on the PR head.

@mikehardy mikehardy added the Needs Author Reply Waiting for a reply from the original author label Oct 26, 2026
@criticalAY
criticalAY force-pushed the feat/export-deck-selection branch from 4fbcbb5 to d10fd57 Compare October 30, 2026 05:12
@criticalAY
criticalAY requested a review from mikehardy October 30, 2026 05:15
@criticalAY criticalAY removed the Needs Author Reply Waiting for a reply from the original author label Oct 30, 2026
david-allison

This comment was marked as resolved.

@mikehardy mikehardy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assisted-by: grok-4.7

Thanks for the keyboard and screenshot follow-through — the API 24/25 cases are the right place to lock focusable, and the deck screenshot is the screen this control change actually affects.

Locally I rebased cleanly onto current main without pushing (push: no). Lint, unit, and emulator lanes all passed on emulator-5556 (API35_GAPI).

initializeDecks no longer fills a Spinner. It registers the child result listener, sets the click listener and an accessibility delegate that reports Spinner plus canOpenPopup, then loads one deck on lifecycleScope: the parcelled SelectableDeck, otherwise decks.get(deckId, default = false) mapped to SelectableDeck.Deck, otherwise SelectableDeck.AllDecks. showSelectedDeck sets deckSelector.text from getFullDisplayName. A passed deck id still preselects the apkg export type. Fragment.startDeckSelection now returns that Job; the activity overload is unchanged. DeckSelectionDialog.onNewDeckCreated notifies deck and study-queue changes with the dialog as initiator before selectDeckAndClose.

ExportDialogFragmentKeyboardTest inflates dialog_export_options under Theme_Light with the backend unloaded, sets a click listener (so the row is clickable, as production does), and asserts isFocusable on SDK 24, 25, and 36. ExportDialogFragmentScreenshotTest.deck launches newInstance(DEFAULT_DECK_ID) and captures after advanceRobolectricLooper(). ExportDialogFragmentTest covers opening the child dialog (ARG_ALLOW_ALL and ARG_ALLOW_FILTERED), keeping the chosen name across recreate(), selecting a filtered deck, the accessibility role, deck creation showing up on Deck Picker after cancel, dismiss during a held collection load, and a double tap while that load is held opening one dialog.

some acceptable findings, no action required, just leaving for posterity: purpose text vs filtered-deck defaults; 58-char commit title; keyboard test does not call requestFocus(); Google Accessibility Scanner checkbox still open.

Screenshot changes have been detected, so leaving this for a good manual look.

@criticalAY
criticalAY force-pushed the feat/export-deck-selection branch from d10fd57 to a704837 Compare October 30, 2026 17:01
@criticalAY criticalAY removed the Needs Author Reply Waiting for a reply from the original author label Nov 2, 2026
@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Nov 2, 2026
@criticalAY criticalAY removed the Needs Author Reply Waiting for a reply from the original author label Nov 2, 2026
@criticalAY

Copy link
Copy Markdown
Contributor Author

Any reason for the tag?

@david-allison

Copy link
Copy Markdown
Member

Apologies. No need for it. My mistake

david-allison

This comment was marked as resolved.

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Nov 2, 2026
@criticalAY
criticalAY force-pushed the feat/export-deck-selection branch from aef2532 to c35a0c8 Compare November 2, 2026 21:34
@criticalAY criticalAY removed the Needs Author Reply Waiting for a reply from the original author label Nov 2, 2026
david-allison

This comment was marked as resolved.

@david-allison david-allison left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; only nitpicks


private fun getDeckNames(): ArrayList<SelectableDeck> =
BundleCompat.getParcelableArrayList(requireArguments(), ARG_DECK_NAMES, SelectableDeck::class.java)!!
private fun getDeckIds(): LongArray = requireArguments().getLongArray(ARG_DECK_IDS)!!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a requireLongArray extension; we have others. They print the key which causes the failure.

}

private fun showSearchResults() {
allDecksList.filter { currentFilters.accept(it.fullDeckName) }.forEach { it.collapsed = false }

Copy link
Copy Markdown
Member

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

Comment on lines +304 to +309
val remainingDeckCount: Int
get() = allDecksList.size

private val allDecksList = ArrayList<DeckNode>()
private val currentlyDisplayedDecks = ArrayList<DeckNode>()
private var currentFilters = DeckFilters.create("")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 🤷

@david-allison david-allison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I like the 'include' label; I'm not sure it implies that it's a deck that you're including

@criticalAY criticalAY added Needs Author Reply Waiting for a reply from the original author Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) labels Nov 2, 2026
Exporting a deck listed every deck in one long drop-down, which was
hard to use with many subdecks. The deck field now opens the same deck
dialog as the Browser and Statistics, with search and collapsible
subdecks. A deck created from that dialog shows up in the Deck Picker
straight away.

The deck dialog now keeps only deck ids in its arguments, so its saved
state stays small with thousands of decks.

Fixes 18525

Assisted-by: Claude Opus 5.5
Typing a search before the deck list had loaded, or expanding or
collapsing a deck in the results, brought back every deck while the
search text was still there. The list now keeps the search applied.

Assisted-by: Claude Opus 5.5
@criticalAY

Copy link
Copy Markdown
Contributor Author

I'd rather keep it matching desktop here, but happy to open an issue if you'd like a clearer label.

@criticalAY
criticalAY force-pushed the feat/export-deck-selection branch from c35a0c8 to 5f55c56 Compare November 3, 2026 05:08
@criticalAY criticalAY removed Needs Author Reply Waiting for a reply from the original author Needs Second Approval Has one approval, one more approval to merge labels Nov 3, 2026
@criticalAY
criticalAY enabled auto-merge November 3, 2026 05:57

@mikehardy mikehardy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assisted-by: grok-4.7

Thanks for carrying this through the search, binder-size, and deck-creation cases — the shared dialog fix is the right place for the filter bug.

Locally I checked out the PR and replayed at the head without pushing (push: no). Lint, unit, and emulator lanes all passed on emulator-5556 (API35_GAPI).

dialog_export_options swaps R.id.deck_selector from a Spinner to a FixedTextView (spinnerItemStyle, dropdown drawable, focusable, selectable background). ExportDialogFragment.initializeDecks registers registerChildDeckSelectedHandler(REQUEST_EXPORT_DECK), ignores a second click while startDeckSelection's Job is active or a child tagged DeckSelectionDialog.TAG is already added, and sets an accessibility delegate that reports Spinner plus canOpenPopup. The label is loaded on lifecycleScope from decks.get(deckId, default = false) (null and unknown ids, including ALL_DECKS_ID 0, become SelectableDeck.AllDecks). onSaveInstanceState stores that id; a later get of a real id reloads the name, so a rename while stopped shows the new name. Export limits use wholeCollection for AllDecks and deck.deckId otherwise. The positive button still returns early when a non-collection export is tapped before the selector is enabled.

Fragment.startDeckSelection returns that Job. The activity overload is unchanged. Dialog arguments are now a LongArray of deck ids (AllDecks stored as 0) via Bundle.requireLongArray. On load, those ids are resolved with deckDueTree().find, so a large collection no longer parcels every SelectableDeck. onNewDeckCreated notifies ChangeManager (deck and studyQueues, dialog as initiator) before selectDeckAndClose, which is what lets Deck Picker show a deck created from the export dialog after export is cancelled. CreateDeckDialog itself only calls decks.id and the callback.

Multi-select dismisses when allDecksList is empty (remainingDeckCount), not when the filtered row count hits zero, so selecting the last search hit leaves the dialog up while non-matching decks remain. DecksFilter.publishResults records DeckFilters.create(constraint) and calls showSearchResults, which clears collapsed on matching nodes and then rebuilds with accept(fullDeckName) && isViewable.

Merge pending CI.

Screenshot changes have been detected, so leaving this for a good manual look.

@mikehardy mikehardy added the Needs Second Approval Has one approval, one more approval to merge label Nov 3, 2026
@criticalAY
criticalAY added this pull request to the merge queue Nov 3, 2026
Merged via the queue into ankidroid:main with commit c33e8d3 Nov 3, 2026
25 checks passed
@github-actions github-actions Bot added this to the 2.26 release milestone Nov 3, 2026
@github-actions github-actions Bot removed Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) Needs Second Approval Has one approval, one more approval to merge labels Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use the "Deck Search" pop-up in the export/import

3 participants