diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 6f61c04ddaec..b745ea4ec091 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -800,23 +800,32 @@ open class DeckPicker : Timber.d("onCreateOptionsMenu()") floatingActionMenu.closeFloatingActionMenu(applyRiseAndShrinkAnimation = false) menuInflater.inflate(R.menu.deck_picker, menu) - menu.findItem(R.id.action_export)?.title = TR.exportingExport() - setupMediaSyncMenuItem(menu) menu.findItem(R.id.deck_picker_action_filter)?.let { toolbarSearchItem = it setupSearchIcon(it) toolbarSearchView = it.actionView as SearchView } toolbarSearchView?.maxWidth = Integer.MAX_VALUE - // redraw menu synchronously to avoid flicker - updateMenuFromState(menu) + + if (fragmented && studyoptionsFrame!!.visibility == View.VISIBLE) { + menu.setGroupVisible(R.id.allItems, false) + } else { + menu.findItem(R.id.action_export)?.title = TR.exportingExport() + setupMediaSyncMenuItem(menu) + // redraw menu synchronously to avoid flicker + updateMenuFromState(menu) + } // ...then launch a task to possibly update the visible icons. // Store the job so that tests can easily await it. In the future // this may be better done by injecting a custom test scheduler // into CollectionManager, and awaiting that. createMenuJob = launchCatchingTask { updateMenuState() - updateMenuFromState(menu) + if (fragmented) { + updateSearchFromState(menu) + } else { + updateMenuFromState(menu) + } } return super.onCreateOptionsMenu(menu) } @@ -824,7 +833,7 @@ open class DeckPicker : private var migrationProgressPublishingJob: Job? = null private var cachedMigrationProgressMenuItemActionView: View? = null - private fun setupMediaSyncMenuItem(menu: Menu) { + public fun setupMediaSyncMenuItem(menu: Menu) { // shouldn't be necessary, but `invalidateOptionsMenu()` is called way more than necessary syncMediaProgressJob?.cancel() @@ -970,7 +979,7 @@ open class DeckPicker : searchDecksIcon = menuItem } - private fun updateMenuFromState(menu: Menu) { + fun updateMenuFromState(menu: Menu) { menu.setGroupVisible(R.id.allItems, optionsMenuState != null) optionsMenuState?.run { menu.findItem(R.id.deck_picker_action_filter).isVisible = searchIcon @@ -981,6 +990,12 @@ open class DeckPicker : } } + private fun updateSearchFromState(menu: Menu) { + optionsMenuState?.run { + menu.findItem(R.id.deck_picker_action_filter).isVisible = searchIcon + } + } + private fun updateUndoLabelFromState(menuItem: MenuItem, undoLabel: String?) { menuItem.run { if (undoLabel != null) { @@ -1639,7 +1654,7 @@ open class DeckPicker : return previous } - private fun undo() { + fun undo() { launchCatchingTask { undoAndShowSnackbar() } @@ -2072,7 +2087,10 @@ open class DeckPicker : Timber.i("Updating deck list UI") hideProgressBar() // Make sure the fragment is visible - if (fragmented) { + if (fragmented && collectionIsEmpty) { + studyoptionsFrame!!.visibility = View.GONE + invalidateOptionsMenu() + } else if (fragmented) { studyoptionsFrame!!.visibility = View.VISIBLE } dueTree = result @@ -2321,7 +2339,7 @@ open class DeckPicker : updateDeckList() } - private fun handleEmptyCards() { + fun handleEmptyCards() { launchCatchingTask { val emptyCids = withProgress(R.string.emtpy_cards_finding) { withCol { @@ -2522,7 +2540,7 @@ open class DeckPicker : * @return shownAutomatically `true` if the dialog was shown automatically, `false` if the user * pressed a button to open the dialog */ - private fun showDialogThatOffersToMigrateStorage(shownAutomatically: Boolean) { + fun showDialogThatOffersToMigrateStorage(shownAutomatically: Boolean) { Timber.i("Displaying dialog to migrate storage") if (mediaMigrationIsInProgress(baseContext)) { // This should not occur. We should have not called the function in this case. @@ -2579,7 +2597,7 @@ open class DeckPicker : dialog.addScopedStorageLearnMoreLinkAndShow(message) } - private fun showDialogThatOffersToResumeMigrationAfterError(errorText: String) { + fun showDialogThatOffersToResumeMigrationAfterError(errorText: String) { val helpUrl = getString(R.string.link_migration_failed_dialog_learn_more_en) val message = getString(R.string.migration__resume_after_failed_dialog__message, errorText, helpUrl) .parseAsHtml() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt index 0420a859afee..f05ad6a7e60e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt @@ -35,7 +35,12 @@ import androidx.core.view.MenuItemCompat import androidx.fragment.app.Fragment import anki.collection.OpChanges import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.anki.dialogs.DatabaseErrorDialog +import com.ichi2.anki.dialogs.MediaCheckDialog import com.ichi2.anki.dialogs.customstudy.CustomStudyDialog +import com.ichi2.anki.notetype.ManageNotetypes +import com.ichi2.anki.services.MediaMigrationState +import com.ichi2.anki.services.getMediaMigrationState import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.utils.ext.description import com.ichi2.annotations.NeedsTest @@ -81,6 +86,9 @@ class StudyOptionsFragment : Fragment(), ChangeManager.Subscriber, Toolbar.OnMen private lateinit var textTotal: TextView private var toolbar: Toolbar? = null + @VisibleForTesting + var createMenuJob: Job? = null + // Flag to indicate if the fragment should load the deck options immediately after it loads private var loadWithDeckOptions = false private var fragmented = false @@ -223,7 +231,7 @@ class StudyOptionsFragment : Fragment(), ChangeManager.Subscriber, Toolbar.OnMen override fun onMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { - R.id.action_undo -> { + R.id.action_undo1 -> { Timber.i("StudyOptionsFragment:: Undo button pressed") launchCatchingTask { requireActivity().undoAndShowSnackbar() @@ -278,6 +286,66 @@ class StudyOptionsFragment : Fragment(), ChangeManager.Subscriber, Toolbar.OnMen (activity as DeckPicker).exportDeck(col!!.decks.selected()) return true } + R.id.action_undo -> { + Timber.i("DeckPicker:: Undo button pressed") + (activity as DeckPicker).undo() + return true + } + R.id.action_sync -> { + Timber.i("DeckPicker:: Sync button pressed") + val actionProvider = MenuItemCompat.getActionProvider(item) as? SyncActionProvider + if (actionProvider?.isProgressShown == true) { + launchCatchingTask { + monitorMediaSync(requireContext()) + } + } else { + (activity as DeckPicker).sync() + } + return true + } + R.id.action_scoped_storage_migrate -> { + Timber.i("DeckPicker:: migrate button pressed") + val migrationState = (activity as DeckPicker).getMediaMigrationState() + if (migrationState is MediaMigrationState.Ongoing.PausedDueToError) { + (activity as DeckPicker).showDialogThatOffersToResumeMigrationAfterError(migrationState.errorText) + } else { + (activity as DeckPicker).showDialogThatOffersToMigrateStorage(shownAutomatically = false) + } + return true + } + R.id.action_import -> { + Timber.i("DeckPicker:: Import button pressed") + (activity as DeckPicker).showImportDialog() + return true + } + R.id.action_check_database -> { + Timber.i("DeckPicker:: Check database button pressed") + (activity as DeckPicker).showDatabaseErrorDialog(DatabaseErrorDialog.DatabaseErrorDialogType.DIALOG_CONFIRM_DATABASE_CHECK) + return true + } + R.id.action_check_media -> { + Timber.i("DeckPicker:: Check media button pressed") + (activity as DeckPicker).showMediaCheckDialog(MediaCheckDialog.DIALOG_CONFIRM_MEDIA_CHECK) + return true + } + R.id.action_empty_cards -> { + Timber.i("DeckPicker:: Empty cards button pressed") + (activity as DeckPicker).handleEmptyCards() + return true + } + R.id.action_model_browser_open -> { + Timber.i("DeckPicker:: Model browser button pressed") + val manageNoteTypesTarget = + ManageNotetypes::class.java + val noteTypeBrowser = Intent(requireContext(), manageNoteTypesTarget) + startActivity(noteTypeBrowser) + return true + } + R.id.action_restore_backup -> { + Timber.i("DeckPicker:: Restore from backup button pressed") + (activity as DeckPicker).showDatabaseErrorDialog(DatabaseErrorDialog.DatabaseErrorDialogType.DIALOG_CONFIRM_RESTORE_BACKUP) + return true + } else -> return false } } @@ -335,9 +403,17 @@ class StudyOptionsFragment : Fragment(), ChangeManager.Subscriber, Toolbar.OnMen } // Switch on rename / delete / export if tablet layout if (fragmented) { + menu.setGroupVisible(R.id.allItems, true) menu.findItem(R.id.action_rename).isVisible = true menu.findItem(R.id.action_delete).isVisible = true menu.findItem(R.id.action_export).isVisible = true + (activity as DeckPicker).setupMediaSyncMenuItem(menu) + (activity as DeckPicker).updateMenuFromState(menu) + + createMenuJob = launchCatchingTask { + (activity as DeckPicker).updateMenuState() + (activity as DeckPicker).updateMenuFromState(menu) + } } else { menu.findItem(R.id.action_rename).isVisible = false menu.findItem(R.id.action_delete).isVisible = false @@ -347,15 +423,15 @@ class StudyOptionsFragment : Fragment(), ChangeManager.Subscriber, Toolbar.OnMen menu.findItem(R.id.action_unbury).isVisible = col != null && col!!.sched.haveBuried() // Set the proper click target for the undo button's ActionProvider val undoActionProvider: RtlCompliantActionProvider? = MenuItemCompat.getActionProvider( - menu.findItem(R.id.action_undo) + menu.findItem(R.id.action_undo1) ) as? RtlCompliantActionProvider undoActionProvider?.clickHandler = { _, menuItem -> onMenuItemClick(menuItem) } // Switch on or off undo depending on whether undo is available if (col == null || !col!!.undoAvailable()) { - menu.findItem(R.id.action_undo).isVisible = false + menu.findItem(R.id.action_undo1).isVisible = false } else { - menu.findItem(R.id.action_undo).isVisible = true - menu.findItem(R.id.action_undo).title = col?.undoLabel() + menu.findItem(R.id.action_undo1).isVisible = true + menu.findItem(R.id.action_undo1).title = col?.undoLabel() } // Set the back button listener if (!fragmented) { @@ -363,6 +439,8 @@ class StudyOptionsFragment : Fragment(), ChangeManager.Subscriber, Toolbar.OnMen icon!!.isAutoMirrored = true toolbar!!.navigationIcon = icon toolbar!!.setNavigationOnClickListener { (activity as AnkiActivity).finish() } + } else { + toolbar!!.navigationIcon = null } } catch (e: IllegalStateException) { if (!CollectionManager.isOpenUnsafe()) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt index dda0027c33b6..b482f8e5e7d4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Sync.kt @@ -16,6 +16,7 @@ package com.ichi2.anki +import android.app.Activity import android.app.Activity.RESULT_OK import android.content.Context import android.content.SharedPreferences @@ -353,13 +354,13 @@ fun DeckPicker.shouldFetchMedia(preferences: SharedPreferences): Boolean { } suspend fun monitorMediaSync( - deckPicker: DeckPicker + context: Context ) { val backend = CollectionManager.getBackend() val scope = CoroutineScope(Dispatchers.IO) val dialog = withContext(Dispatchers.Main) { - AlertDialog.Builder(deckPicker) + AlertDialog.Builder(context) .setTitle(TR.syncMediaLogTitle()) .setMessage("") .setPositiveButton(R.string.dialog_continue) { _, _ -> @@ -371,7 +372,7 @@ suspend fun monitorMediaSync( .show() } - fun showMessage(msg: String) = deckPicker.showSnackbar(msg, Snackbar.LENGTH_SHORT) + fun showMessage(msg: String) = (context as Activity).showSnackbar(msg, Snackbar.LENGTH_SHORT) scope.launch { try { diff --git a/AnkiDroid/src/main/res/menu/deck_picker.xml b/AnkiDroid/src/main/res/menu/deck_picker.xml index 2e0aeb2758cb..63903ca4d933 100644 --- a/AnkiDroid/src/main/res/menu/deck_picker.xml +++ b/AnkiDroid/src/main/res/menu/deck_picker.xml @@ -2,13 +2,13 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:ankidroid="http://schemas.android.com/apk/res-auto" tools:context=".DeckPicker"> + - + + + + + + + + + + + + + + + + + +