diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index 0b8f0185cd8a..b3af1705f3a7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -713,6 +713,7 @@ open class CardBrowser : // restore drawer click listener and icon restoreDrawerIcon() menuInflater.inflate(R.menu.card_browser, menu) + addFlags(menu.findItem(R.id.action_search_by_flag).subMenu, Mode.SINGLE_SELECT) saveSearchItem = menu.findItem(R.id.action_save_search) saveSearchItem?.isVisible = false // the searchview's query always starts empty. mySearchesItem = menu.findItem(R.id.action_list_my_searches) @@ -766,6 +767,7 @@ open class CardBrowser : } else { // multi-select mode menuInflater.inflate(R.menu.card_browser_multiselect, menu) + addFlags(menu.findItem(R.id.action_flag).subMenu, Mode.MULTI_SELECT) showBackIcon() increaseHorizontalPaddingOfOverflowMenuIcons(menu) } @@ -791,6 +793,30 @@ open class CardBrowser : return super.onCreateOptionsMenu(menu) } + /** + * Representing different selection modes. + */ + enum class Mode(val value: Int) { + SINGLE_SELECT(1000), + MULTI_SELECT(1001) + } + + private fun addFlags(subMenu: SubMenu?, mode: Mode) { + lifecycleScope.launch { + val flagNames = getFlagData(this@CardBrowser) + + val groupId = when (mode) { + Mode.SINGLE_SELECT -> mode.value + Mode.MULTI_SELECT -> mode.value + } + + flagNames.forEach { (flag, pair) -> + val (title, drawableRes) = pair + subMenu?.add(groupId, flag.ordinal, Menu.NONE, title)?.setIcon(drawableRes) + } + } + } + override fun onNavigationPressed() { if (viewModel.isInMultiSelectMode) { viewModel.endMultiSelectMode() @@ -900,6 +926,16 @@ open class CardBrowser : undoSnackbar != null && undoSnackbar!!.isShown -> undoSnackbar!!.dismiss() } + val flag = Flag.entries.find { it.ordinal == item.itemId } + flag?.let { + when (item.groupId) { + Mode.SINGLE_SELECT.value -> filterByFlag(it) + Mode.MULTI_SELECT.value -> updateFlagForSelectedRows(it) + else -> return@let + } + return true + } + when (item.itemId) { android.R.id.home -> { viewModel.endMultiSelectMode() @@ -962,70 +998,6 @@ open class CardBrowser : showFilterByTagsDialog() return true } - R.id.action_flag_zero -> { - updateFlagForSelectedRows(Flag.NONE) - return true - } - R.id.action_flag_one -> { - updateFlagForSelectedRows(Flag.RED) - return true - } - R.id.action_flag_two -> { - updateFlagForSelectedRows(Flag.ORANGE) - return true - } - R.id.action_flag_three -> { - updateFlagForSelectedRows(Flag.GREEN) - return true - } - R.id.action_flag_four -> { - updateFlagForSelectedRows(Flag.BLUE) - return true - } - R.id.action_flag_five -> { - updateFlagForSelectedRows(Flag.PINK) - return true - } - R.id.action_flag_six -> { - updateFlagForSelectedRows(Flag.TURQUOISE) - return true - } - R.id.action_flag_seven -> { - updateFlagForSelectedRows(Flag.PURPLE) - return true - } - R.id.action_select_flag_zero -> { - filterByFlag(Flag.NONE) - return true - } - R.id.action_select_flag_one -> { - filterByFlag(Flag.RED) - return true - } - R.id.action_select_flag_two -> { - filterByFlag(Flag.ORANGE) - return true - } - R.id.action_select_flag_three -> { - filterByFlag(Flag.GREEN) - return true - } - R.id.action_select_flag_four -> { - filterByFlag(Flag.BLUE) - return true - } - R.id.action_select_flag_five -> { - filterByFlag(Flag.PINK) - return true - } - R.id.action_select_flag_six -> { - filterByFlag(Flag.TURQUOISE) - return true - } - R.id.action_select_flag_seven -> { - filterByFlag(Flag.PURPLE) - return true - } R.id.action_delete_card -> { deleteSelectedNotes() return true diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt index fb0464b4df93..d0ec1cee297b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Flag.kt @@ -15,11 +15,26 @@ */ package com.ichi2.anki +import android.content.Context import androidx.annotation.ColorRes import androidx.annotation.DrawableRes +import com.ichi2.anki.CollectionManager.withCol import com.ichi2.libanki.Card import com.ichi2.libanki.CardId import com.ichi2.libanki.Collection +import org.json.JSONObject + +// this is not part of the enum as getFlagNames should be used to handle overrides +private val flagToName = hashMapOf( + Flag.NONE to R.string.menu_flag_card_zero, + Flag.RED to R.string.menu_flag_card_one, + Flag.ORANGE to R.string.menu_flag_card_two, + Flag.GREEN to R.string.menu_flag_card_three, + Flag.BLUE to R.string.menu_flag_card_four, + Flag.PINK to R.string.menu_flag_card_five, + Flag.TURQUOISE to R.string.menu_flag_card_six, + Flag.PURPLE to R.string.menu_flag_card_seven +) enum class Flag(val code: Int, @DrawableRes val drawableRes: Int, @ColorRes val browserColorRes: Int?) { NONE(0, R.drawable.ic_flag_transparent, null), @@ -39,3 +54,28 @@ enum class Flag(val code: Int, @DrawableRes val drawableRes: Int, @ColorRes val } fun Collection.setUserFlag(flag: Flag, cids: List) = this.setUserFlag(flag.code, cids) fun Card.setUserFlag(flag: Flag) = this.setUserFlag(flag.code) + +/** + * Retrieves the flag data, including flag names and corresponding drawable resource IDs. + * This method asynchronously fetches the flag names and drawable resource IDs for each flag. + * + * @param context The context used to access application resources. + * @return A map where each Flag enum is associated with a pair containing its name as a string and its corresponding drawable resource ID. + */ +suspend fun getFlagData(context: Context): Map> { + val overrides = withCol { config.getObject("flagLabels", JSONObject()) } + return Flag.entries.associateWith { flag -> + val flagName = overrides.getStringOrNull(flag.code.toString()) ?: context.getString(flagToName[flag]!!) + val drawableResId = flag.drawableRes + flagName to drawableResId + } +} + +private fun JSONObject.getStringOrNull(key: String): String? { + if (!has(key)) return null + return try { + getString(key) + } catch (_: Exception) { + null + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index 604d33f1e75e..85f7989db4df 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -40,6 +40,7 @@ import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.Toolbar import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import anki.frontend.SetSchedulingStatesRequest import com.google.android.material.color.MaterialColors @@ -88,6 +89,7 @@ import com.ichi2.utils.HandlerUtils.getDefaultLooper import com.ichi2.utils.Permissions.canRecordAudio import com.ichi2.utils.ViewGroupUtils.setRenderWorkaround import com.ichi2.widget.WidgetStatus.updateInBackground +import kotlinx.coroutines.launch import timber.log.Timber import java.io.File @@ -350,6 +352,11 @@ open class Reviewer : if (drawerToggle.onOptionsItemSelected(item)) { return true } + val flag = Flag.entries.find { it.ordinal == item.itemId } + flag?.let { + onFlag(currentCard, it) + return true + } when (item.itemId) { android.R.id.home -> { Timber.i("Reviewer:: Home button pressed") @@ -449,38 +456,6 @@ open class Reviewer : Timber.i("Reviewer:: Add note button pressed") addNote() } - R.id.action_flag_zero -> { - Timber.i("Reviewer:: No flag") - onFlag(currentCard, Flag.NONE) - } - R.id.action_flag_one -> { - Timber.i("Reviewer:: Flag one") - onFlag(currentCard, Flag.RED) - } - R.id.action_flag_two -> { - Timber.i("Reviewer:: Flag two") - onFlag(currentCard, Flag.ORANGE) - } - R.id.action_flag_three -> { - Timber.i("Reviewer:: Flag three") - onFlag(currentCard, Flag.GREEN) - } - R.id.action_flag_four -> { - Timber.i("Reviewer:: Flag four") - onFlag(currentCard, Flag.BLUE) - } - R.id.action_flag_five -> { - Timber.i("Reviewer:: Flag five") - onFlag(currentCard, Flag.PINK) - } - R.id.action_flag_six -> { - Timber.i("Reviewer:: Flag six") - onFlag(currentCard, Flag.TURQUOISE) - } - R.id.action_flag_seven -> { - Timber.i("Reviewer:: Flag seven") - onFlag(currentCard, Flag.PURPLE) - } R.id.action_card_info -> { Timber.i("Card Viewer:: Card Info") openCardInfo() @@ -692,12 +667,28 @@ open class Reviewer : startActivityWithAnimation(intent, animation) } + private val flagItemIds = mutableSetOf() + + private fun addFlags(subMenu: SubMenu?) { + lifecycleScope.launch { + val flagNames = getFlagData(this@Reviewer) + flagNames.forEach { (flag, pair) -> + val (title, drawableRes) = pair + val menuItem = subMenu?.add(Menu.NONE, flag.ordinal, Menu.NONE, title)?.setIcon(drawableRes) + menuItem?.let { + flagItemIds.add(it.itemId) + } + } + } + } + // Related to https://github.com/ankidroid/Anki-Android/pull/11061#issuecomment-1107868455 @NeedsTest("Order of operations needs Testing around Menu (Overflow) Icons and their colors.") override fun onCreateOptionsMenu(menu: Menu): Boolean { Timber.d("onCreateOptionsMenu()") // NOTE: This is called every time a new question is shown via invalidate options menu menuInflater.inflate(R.menu.reviewer, menu) + addFlags(menu.findItem(R.id.action_flag).subMenu) displayIcons(menu) actionButtons.setCustomButtonsStatus(menu) val alpha = Themes.ALPHA_ICON_ENABLED_LIGHT @@ -709,23 +700,6 @@ open class Reviewer : } markCardIcon.iconAlpha = alpha - val flagIcon = menu.findItem(R.id.action_flag) - if (flagIcon != null) { - if (currentCard != null) { - when (currentCard!!.userFlag()) { - 1 -> flagIcon.setIcon(R.drawable.ic_flag_red) - 2 -> flagIcon.setIcon(R.drawable.ic_flag_orange) - 3 -> flagIcon.setIcon(R.drawable.ic_flag_green) - 4 -> flagIcon.setIcon(R.drawable.ic_flag_blue) - 5 -> flagIcon.setIcon(R.drawable.ic_flag_pink) - 6 -> flagIcon.setIcon(R.drawable.ic_flag_turquoise) - 7 -> flagIcon.setIcon(R.drawable.ic_flag_purple) - else -> flagIcon.setIcon(R.drawable.ic_flag_transparent) - } - } - flagIcon.iconAlpha = alpha - } - // Anki Desktop Translations menu.findItem(R.id.action_reschedule_card).title = CollectionManager.TR.actionsSetDueDate().toSentenceCase(R.string.sentence_set_due_date) @@ -849,11 +823,14 @@ open class Reviewer : onboarding.onCreate() increaseHorizontalPaddingOfOverflowMenuIcons(menu) - tintOverflowMenuIcons(menu, skipIf = { isFlagResource(it.itemId) }) - + tintOverflowMenuIcons(menu, skipIf = { isFlagItem(it) }) return super.onCreateOptionsMenu(menu) } + private fun isFlagItem(menuItem: MenuItem): Boolean { + return flagItemIds.contains(menuItem.itemId) + } + @SuppressLint("RestrictedApi") private fun displayIcons(menu: Menu) { try { @@ -867,10 +844,6 @@ open class Reviewer : } } - private fun isFlagResource(itemId: Int): Boolean { - return itemId == R.id.action_flag_seven || itemId == R.id.action_flag_six || itemId == R.id.action_flag_five || itemId == R.id.action_flag_four || itemId == R.id.action_flag_three || itemId == R.id.action_flag_two || itemId == R.id.action_flag_one - } - override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { if (answerFieldIsFocused()) { return super.onKeyDown(keyCode, event) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt index 2b3cb0fe07bd..ecfec0265a1e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt @@ -41,6 +41,7 @@ import com.ichi2.anki.Flag import com.ichi2.anki.R import com.ichi2.anki.browser.PreviewerIdsFile import com.ichi2.anki.cardviewer.CardMediaPlayer +import com.ichi2.anki.getFlagData import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider import com.ichi2.anki.snackbar.SnackbarBuilder import com.ichi2.anki.utils.ext.sharedPrefs @@ -122,6 +123,15 @@ class PreviewerFragment : } } + lifecycleScope.launch { + val flagData = getFlagData(requireContext()) + val submenu = menu.findItem(R.id.action_flag).subMenu + flagData.forEach { (flag, pair) -> + val (title, drawableRes) = pair + submenu?.add(Menu.NONE, flag.ordinal, Menu.NONE, title)?.setIcon(drawableRes) + } + } + lifecycleScope.launch { viewModel.flagCode .flowWithLifecycle(lifecycle) @@ -188,18 +198,15 @@ class PreviewerFragment : } override fun onMenuItemClick(item: MenuItem): Boolean { + val flag = Flag.entries.find { it.ordinal == item.itemId } + flag?.let { + viewModel.setFlag(it) + return true + } when (item.itemId) { R.id.action_edit -> editCard() R.id.action_mark -> viewModel.toggleMark() R.id.action_back_side_only -> viewModel.toggleBackSideOnly() - R.id.action_flag_zero -> viewModel.setFlag(Flag.NONE) - R.id.action_flag_one -> viewModel.setFlag(Flag.RED) - R.id.action_flag_two -> viewModel.setFlag(Flag.ORANGE) - R.id.action_flag_three -> viewModel.setFlag(Flag.GREEN) - R.id.action_flag_four -> viewModel.setFlag(Flag.BLUE) - R.id.action_flag_five -> viewModel.setFlag(Flag.PINK) - R.id.action_flag_six -> viewModel.setFlag(Flag.TURQUOISE) - R.id.action_flag_seven -> viewModel.setFlag(Flag.PURPLE) } return true } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Config.kt b/AnkiDroid/src/main/java/com/ichi2/libanki/Config.kt index ae110942988b..4181ec535bb3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Config.kt +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Config.kt @@ -25,6 +25,7 @@ import kotlinx.serialization.json.Json import net.ankiweb.rsdroid.Backend import net.ankiweb.rsdroid.exceptions.BackendNotFoundException import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject class Config(val backend: Backend) { @@ -69,4 +70,15 @@ class Config(val backend: Backend) { null } } + + @NotInLibAnki + fun getObject(key: String, default: JSONObject): JSONObject { + return try { + JSONObject(backend.getConfigJson(key).toStringUtf8()) + } catch (ex: BackendNotFoundException) { + default + } catch (ex: JSONException) { + default + } + } } diff --git a/AnkiDroid/src/main/res/menu/card_browser.xml b/AnkiDroid/src/main/res/menu/card_browser.xml index df6f66f27417..0f50569e50ca 100644 --- a/AnkiDroid/src/main/res/menu/card_browser.xml +++ b/AnkiDroid/src/main/res/menu/card_browser.xml @@ -38,37 +38,7 @@ android:id="@+id/action_search_by_flag" android:title="@string/card_browser_search_by_flag"> - - - - - - - - + diff --git a/AnkiDroid/src/main/res/menu/card_browser_multiselect.xml b/AnkiDroid/src/main/res/menu/card_browser_multiselect.xml index 3730012fef6a..65212d02bd7e 100644 --- a/AnkiDroid/src/main/res/menu/card_browser_multiselect.xml +++ b/AnkiDroid/src/main/res/menu/card_browser_multiselect.xml @@ -25,37 +25,7 @@ android:title="@string/menu_flag_card" android:icon="@drawable/ic_flag_transparent"> - - - - - - - - + diff --git a/AnkiDroid/src/main/res/menu/previewer.xml b/AnkiDroid/src/main/res/menu/previewer.xml index 7643e7472d9b..bd66333059a9 100644 --- a/AnkiDroid/src/main/res/menu/previewer.xml +++ b/AnkiDroid/src/main/res/menu/previewer.xml @@ -27,37 +27,7 @@ android:icon="@drawable/ic_flag_transparent" app:showAsAction="always" > - - - - - - - - + - - - - - - - - + scenario.onActivity { reviewer: Reviewer -> assertFailsWith { reviewer.getColUnsafe } } } - } @Ignore("flaky") @Test