From 059eba6b6801bb2634d7b8e5918cf284649a3dc5 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:20:38 +0900 Subject: [PATCH 01/19] refactor: reposition NoteTypeFieldEditor.kt --- AnkiDroid/src/main/AndroidManifest.xml | 2 +- .../dialogs/NoteTypeFieldEditorContextMenu.kt | 2 +- .../anki/notetype/ManageNoteTypesState.kt | 2 +- .../fieldeditor}/NoteTypeFieldEditor.kt | 78 ++++++++++++------- .../com/ichi2/anki/NoteTypeFieldEditorTest.kt | 12 ++- .../java/com/ichi2/testutils/ActivityList.kt | 2 +- 6 files changed, 63 insertions(+), 35 deletions(-) rename AnkiDroid/src/main/java/com/ichi2/anki/{ => notetype/fieldeditor}/NoteTypeFieldEditor.kt (85%) diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml index 6185173afd9d..b7a50f9d32f1 100644 --- a/AnkiDroid/src/main/AndroidManifest.xml +++ b/AnkiDroid/src/main/AndroidManifest.xml @@ -366,7 +366,7 @@ android:exported="false" /> diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenu.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenu.kt index 5e3db068d938..cdb02b1d5194 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenu.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenu.kt @@ -9,9 +9,9 @@ import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf -import com.ichi2.anki.NoteTypeFieldEditor import com.ichi2.anki.R import com.ichi2.anki.analytics.AnalyticsDialogFragment +import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor import com.ichi2.utils.create import timber.log.Timber diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesState.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesState.kt index d054b461a26c..35f7f460ba75 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesState.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesState.kt @@ -23,8 +23,8 @@ import anki.notetypes.Notetype import anki.notetypes.NotetypeNameIdUseCount import com.google.android.material.snackbar.Snackbar import com.ichi2.anki.CardTemplateEditor -import com.ichi2.anki.NoteTypeFieldEditor import com.ichi2.anki.libanki.NoteTypeId +import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor import com.ichi2.anki.utils.Destination /** Encapsulates the entire state for [ManageNotetypes] */ diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt similarity index 85% rename from AnkiDroid/src/main/java/com/ichi2/anki/NoteTypeFieldEditor.kt rename to AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 926ea7b70802..2482ef056e50 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -package com.ichi2.anki +package com.ichi2.anki.notetype.fieldeditor import android.content.Context import android.os.Bundle @@ -41,6 +41,7 @@ import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.KEY_SELECTED_LOCAL import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.REQUEST_HINT_LOCALE_SELECTION import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.Companion.newInstance import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.NoteTypeFieldEditorContextMenuAction +import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.libanki.Collection import com.ichi2.anki.libanki.Fields import com.ichi2.anki.libanki.NotetypeJson @@ -51,6 +52,7 @@ import com.ichi2.anki.utils.ext.dismissAllDialogFragments import com.ichi2.anki.utils.ext.setCompoundDrawablesRelativeWithIntrinsicBoundsKt import com.ichi2.anki.utils.ext.setFragmentResultListener import com.ichi2.anki.utils.ext.showDialogFragment +import com.ichi2.anki.withProgress import com.ichi2.ui.FixedEditText import com.ichi2.utils.customView import com.ichi2.utils.getInputField @@ -67,7 +69,7 @@ import timber.log.Timber import java.util.Locale @NeedsTest("perform one action, then another") -class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { +class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.ichi2.anki.R.layout.note_type_field_editor) { private val binding by viewBinding(NoteTypeFieldEditorBinding::bind) // Position of the current field selected @@ -95,7 +97,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { return } super.onCreate(savedInstanceState) - setContentView(R.layout.note_type_field_editor) + setContentView(_root_ide_package_.com.ichi2.anki.R.layout.note_type_field_editor) enableToolbar() binding.notetypeName.text = intent.getStringExtra(EXTRA_NOTETYPE_NAME) startLoadingCollection() @@ -134,7 +136,11 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { val noteTypeID = intent.getLongExtra(EXTRA_NOTETYPE_ID, 0) val collectionModel = getColUnsafe.notetypes.get(noteTypeID) if (collectionModel == null) { - showThemedToast(this, R.string.field_editor_model_not_available, true) + _root_ide_package_.com.ichi2.anki.showThemedToast( + this, + _root_ide_package_.com.ichi2.anki.R.string.field_editor_model_not_available, + true, + ) finish() return } @@ -173,11 +179,19 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { } input = input.substring(offset).trim() if (input.isEmpty()) { - showThemedToast(this, resources.getString(R.string.toast_empty_name), true) + _root_ide_package_.com.ichi2.anki.showThemedToast( + this, + resources.getString(_root_ide_package_.com.ichi2.anki.R.string.toast_empty_name), + true, + ) return null } if (fieldsLabels.any { input == it }) { - showThemedToast(this, resources.getString(R.string.toast_duplicate_field), true) + _root_ide_package_.com.ichi2.anki.showThemedToast( + this, + resources.getString(_root_ide_package_.com.ichi2.anki.R.string.toast_duplicate_field), + true, + ) return null } return input @@ -195,8 +209,8 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { fieldNameInput.isSingleLine = true AlertDialog.Builder(this).show { customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) - title(R.string.model_field_editor_add) - positiveButton(R.string.menu_add) { + title(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_add) + positiveButton(_root_ide_package_.com.ichi2.anki.R.string.menu_add) { // Name is valid, now field is added val fieldName = uniqueName(fieldNameInput) try { @@ -206,7 +220,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { // Create dialogue to for schema change val c = ConfirmationDialog() - c.setArgs(resources.getString(R.string.full_sync_confirmation)) + c.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) val confirm = Runnable { try { @@ -222,7 +236,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { getColUnsafe.notetypes.update(notetype) initialize() } - negativeButton(R.string.dialog_cancel) + negativeButton(_root_ide_package_.com.ichi2.anki.R.string.dialog_cancel) } } } @@ -270,7 +284,11 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { } if (fieldsLabels.size < 2) { - showThemedToast(this, resources.getString(R.string.toast_last_field), true) + _root_ide_package_.com.ichi2.anki.showThemedToast( + this, + resources.getString(_root_ide_package_.com.ichi2.anki.R.string.toast_last_field), + true, + ) } else { try { getColUnsafe.modSchema(check = true) @@ -278,7 +296,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { ConfirmationDialog().let { it.setArgs( title = fieldName, - message = resources.getString(R.string.field_delete_warning), + message = resources.getString(_root_ide_package_.com.ichi2.anki.R.string.field_delete_warning), ) it.setConfirm(confirm) showDialogFragment(it) @@ -287,7 +305,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { e.log() ConfirmationDialog().let { it.setConfirm(confirm) - it.setArgs(resources.getString(R.string.full_sync_confirmation)) + it.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) showDialogFragment(it) } } @@ -297,7 +315,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { private fun deleteField() { launchCatchingTask { Timber.d("doInBackGroundDeleteField") - withProgress(message = getString(R.string.model_field_editor_changing)) { + withProgress(message = getString(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_changing)) { val result = withCol { try { @@ -329,8 +347,8 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { fieldNameInput.moveCursorToEnd() AlertDialog.Builder(this).show { customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) - title(R.string.model_field_editor_rename) - positiveButton(R.string.rename) { + title(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_rename) + positiveButton(_root_ide_package_.com.ichi2.anki.R.string.rename) { if (uniqueName(fieldNameInput) == null) { return@positiveButton } @@ -342,7 +360,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { // Handler mod schema confirmation val c = ConfirmationDialog() - c.setArgs(resources.getString(R.string.full_sync_confirmation)) + c.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) val confirm = Runnable { getColUnsafe.modSchema(check = false) @@ -357,7 +375,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { this@NoteTypeFieldEditor.showDialogFragment(c) } } - negativeButton(R.string.dialog_cancel) + negativeButton(_root_ide_package_.com.ichi2.anki.R.string.dialog_cancel) } } } @@ -379,13 +397,13 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { AlertDialog .Builder(this) .show { - positiveButton(R.string.dialog_ok) { + positiveButton(_root_ide_package_.com.ichi2.anki.R.string.dialog_ok) { val input = (it as AlertDialog).getInputField() result(input.text.toString().toInt()) } - negativeButton(R.string.dialog_cancel) + negativeButton(_root_ide_package_.com.ichi2.anki.R.string.dialog_cancel) setMessage(TR.fieldsNewPosition1(numberOfTemplates)) - setView(R.layout.dialog_generic_text_input) + setView(_root_ide_package_.com.ichi2.anki.R.layout.dialog_generic_text_input) }.input( prefill = (currentPos + 1).toString(), inputType = InputType.TYPE_CLASS_NUMBER, @@ -410,7 +428,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { // Handle mod schema confirmation val c = ConfirmationDialog() - c.setArgs(resources.getString(R.string.full_sync_confirmation)) + c.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) val confirm = Runnable { try { @@ -428,7 +446,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { private fun repositionField(index: Int) { launchCatchingTask { - withProgress(message = getString(R.string.model_field_editor_changing)) { + withProgress(message = getString(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_changing)) { val result = withCol { Timber.d("doInBackgroundRepositionField") @@ -475,7 +493,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { e.log() // Handler mMod schema confirmation val c = ConfirmationDialog() - c.setArgs(resources.getString(R.string.full_sync_confirmation)) + c.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) val confirm = Runnable { getColUnsafe.modSchema(check = false) @@ -490,7 +508,7 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { notetype: NotetypeJson, idx: Int, ) { - withProgress(resources.getString(R.string.model_field_editor_changing)) { + withProgress(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_changing)) { withCol { Timber.d("doInBackgroundChangeSortField") notetypes.setSortIndex(notetype, idx) @@ -526,7 +544,11 @@ class NoteTypeFieldEditor : AnkiActivity(R.layout.note_type_field_editor) { */ private fun addFieldLocaleHint(selectedLocale: Locale) { setLanguageHintForField(getColUnsafe.notetypes, notetype, currentPos, selectedLocale) - val format = getString(R.string.model_field_editor_language_hint_dialog_success_result, selectedLocale.displayName) + val format = + getString( + _root_ide_package_.com.ichi2.anki.R.string.model_field_editor_language_hint_dialog_success_result, + selectedLocale.displayName, + ) showSnackbar(format, Snackbar.LENGTH_SHORT) initialize() } @@ -571,7 +593,7 @@ enum class NodetypeKind { internal class NoteFieldAdapter( private val context: Context, labels: List>, -) : ArrayAdapter>(context, 0, labels) { +) : android.widget.ArrayAdapter>(context, 0, labels) { override fun getView( position: Int, convertView: View?, @@ -590,7 +612,7 @@ internal class NoteFieldAdapter( binding.fieldName.setCompoundDrawablesRelativeWithIntrinsicBoundsKt( end = when (kind) { - NodetypeKind.SORT -> R.drawable.ic_sort + NodetypeKind.SORT -> _root_ide_package_.com.ichi2.anki.R.drawable.ic_sort NodetypeKind.UNDEFINED -> 0 }, ) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt index 195da32791a5..a8b62cdf43e5 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt @@ -108,12 +108,18 @@ class NoteTypeFieldEditorTest( // start ModelFieldEditor activity val intent = Intent() - intent.putExtra(NoteTypeFieldEditor.EXTRA_NOTETYPE_NAME, noteTypeName) - intent.putExtra(NoteTypeFieldEditor.EXTRA_NOTETYPE_ID, col.notetypes.idForName(noteTypeName)!!) + intent.putExtra( + _root_ide_package_.com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor.EXTRA_NOTETYPE_NAME, + noteTypeName, + ) + intent.putExtra( + _root_ide_package_.com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor.EXTRA_NOTETYPE_ID, + col.notetypes.idForName(noteTypeName)!!, + ) val noteTypeFieldEditor = startActivityNormallyOpenCollectionWithIntent( this@NoteTypeFieldEditorTest, - NoteTypeFieldEditor::class.java, + _root_ide_package_.com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor::class.java, intent, ) when (fieldOperationType) { diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt index 84ca49b47a32..e8479aa4c9b3 100644 --- a/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt +++ b/AnkiDroid/src/test/java/com/ichi2/testutils/ActivityList.kt @@ -32,7 +32,6 @@ import com.ichi2.anki.IntentHandler.Companion.getReviewDeckIntent import com.ichi2.anki.IntentHandler2 import com.ichi2.anki.IntroductionActivity import com.ichi2.anki.NoteEditorActivity -import com.ichi2.anki.NoteTypeFieldEditor import com.ichi2.anki.Reviewer import com.ichi2.anki.SharedDecksActivity import com.ichi2.anki.SingleFragmentActivity @@ -41,6 +40,7 @@ import com.ichi2.anki.account.AccountActivity import com.ichi2.anki.instantnoteeditor.InstantNoteEditorActivity import com.ichi2.anki.multimedia.MultimediaActivity import com.ichi2.anki.notetype.ManageNotetypes +import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor import com.ichi2.anki.preferences.PreferencesActivity import com.ichi2.anki.previewer.CardViewerActivity import com.ichi2.anki.ui.windows.managespace.ManageSpaceActivity From d205e131e03b5542287618e6d9a20e0f29abb3a3 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sat, 7 Mar 2026 18:16:56 +0900 Subject: [PATCH 02/19] refactor(notetypefieldeditor): move collection operations to viewModel Introduced viewModel to NoteTypeFieldEditor --- .../ichi2/anki/notetype/AddNewNotesType.kt | 7 +- .../fieldeditor/NoteTypeFieldEditor.kt | 211 ++++++------------ .../fieldeditor/NoteTypeFieldEditorState.kt | 14 ++ .../NoteTypeFieldEditorViewModel.kt | 145 ++++++++++++ 4 files changed, 228 insertions(+), 149 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt index f48aed3eb01f..68eeaa397c8e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt @@ -41,6 +41,7 @@ import com.ichi2.utils.customView import com.ichi2.utils.moveCursorToEnd import com.ichi2.utils.negativeButton import com.ichi2.utils.positiveButton +import com.ichi2.utils.show class AddNewNotesType( private val activity: ManageNotetypes, @@ -75,9 +76,9 @@ class AddNewNotesType( val dialog = AlertDialog .Builder(activity) - .apply { + .show { customView(binding.root, paddingStart = 32, paddingEnd = 32, paddingTop = 64, paddingBottom = 64) - positiveButton(R.string.dialog_ok) { _ -> + positiveButton(R.string.menu_add) { _ -> val newName = binding.notetypeNewName.text.toString() val selectedPosition = binding.notetypeNewType.selectedItemPosition if (selectedPosition == AdapterView.INVALID_POSITION) return@positiveButton @@ -89,7 +90,7 @@ class AddNewNotesType( } } negativeButton(R.string.dialog_cancel) - }.show() + } dialog.initializeViewsWith(allOptions, currentNames) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 2482ef056e50..cc599f1f5593 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -23,15 +23,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView -import android.widget.ArrayAdapter import android.widget.EditText +import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.core.os.BundleCompat import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.snackbar.Snackbar import com.ichi2.anki.CollectionManager.TR -import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest import com.ichi2.anki.databinding.ItemNotetypeFieldBinding import com.ichi2.anki.databinding.NoteTypeFieldEditorBinding @@ -43,16 +46,12 @@ import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.Companion.newInstan import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.NoteTypeFieldEditorContextMenuAction import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.libanki.Collection -import com.ichi2.anki.libanki.Fields -import com.ichi2.anki.libanki.NotetypeJson import com.ichi2.anki.libanki.exception.ConfirmModSchemaException -import com.ichi2.anki.servicelayer.LanguageHintService.setLanguageHintForField import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.utils.ext.dismissAllDialogFragments import com.ichi2.anki.utils.ext.setCompoundDrawablesRelativeWithIntrinsicBoundsKt import com.ichi2.anki.utils.ext.setFragmentResultListener import com.ichi2.anki.utils.ext.showDialogFragment -import com.ichi2.anki.withProgress import com.ichi2.ui.FixedEditText import com.ichi2.utils.customView import com.ichi2.utils.getInputField @@ -63,32 +62,19 @@ import com.ichi2.utils.positiveButton import com.ichi2.utils.show import com.ichi2.utils.title import dev.androidbroadcast.vbpd.viewBinding -import org.json.JSONArray +import kotlinx.coroutines.launch import org.json.JSONException import timber.log.Timber import java.util.Locale @NeedsTest("perform one action, then another") -class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.ichi2.anki.R.layout.note_type_field_editor) { +class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field_editor) { private val binding by viewBinding(NoteTypeFieldEditorBinding::bind) + val viewModel by viewModels() // Position of the current field selected - private var currentPos = 0 private var fieldNameInput: EditText? = null - // Backing field for [notetype]. Not with _ because it's only allowed for public field. - private var notetypeBackup: NotetypeJson? = null - private var notetype: NotetypeJson - get() = notetypeBackup!! - set(value) { - notetypeBackup = value - } - - private lateinit var fieldsLabels: List - - // WARN: this should be lateinit, but this can't yet be done on an inline class - private var noteFields: Fields = Fields(JSONArray()) - // ---------------------------------------------------------------------------- // ANDROID METHODS // ---------------------------------------------------------------------------- @@ -97,7 +83,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i return } super.onCreate(savedInstanceState) - setContentView(_root_ide_package_.com.ichi2.anki.R.layout.note_type_field_editor) + setContentView(R.layout.note_type_field_editor) enableToolbar() binding.notetypeName.text = intent.getStringExtra(EXTRA_NOTETYPE_NAME) startLoadingCollection() @@ -133,25 +119,17 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i * to finish the activity. */ private fun initialize() { - val noteTypeID = intent.getLongExtra(EXTRA_NOTETYPE_ID, 0) - val collectionModel = getColUnsafe.notetypes.get(noteTypeID) - if (collectionModel == null) { - _root_ide_package_.com.ichi2.anki.showThemedToast( - this, - _root_ide_package_.com.ichi2.anki.R.string.field_editor_model_not_available, - true, - ) - finish() - return + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.state.collect { + binding.fields.adapter = NoteFieldAdapter(this@NoteTypeFieldEditor, fieldNamesWithKind()) + } + } } - notetype = collectionModel - noteFields = notetype.fields - fieldsLabels = notetype.fieldsNames - binding.fields.adapter = NoteFieldAdapter(this, fieldNamesWithKind()) binding.fields.onItemClickListener = AdapterView.OnItemClickListener { _, _, position: Int, _ -> - showDialogFragment(newInstance(fieldsLabels[position])) - currentPos = position + showDialogFragment(newInstance(viewModel.state.value.fieldsLabels[position])) + viewModel.updateCurrentPosition(position) } binding.btnAdd.setOnClickListener { addFieldDialog() } } @@ -181,15 +159,17 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i if (input.isEmpty()) { _root_ide_package_.com.ichi2.anki.showThemedToast( this, - resources.getString(_root_ide_package_.com.ichi2.anki.R.string.toast_empty_name), + resources.getString(R.string.toast_empty_name), true, ) return null } - if (fieldsLabels.any { input == it }) { + if (viewModel.state.value.fieldsLabels + .any { input == it } + ) { _root_ide_package_.com.ichi2.anki.showThemedToast( this, - resources.getString(_root_ide_package_.com.ichi2.anki.R.string.toast_duplicate_field), + resources.getString(R.string.toast_duplicate_field), true, ) return null @@ -209,8 +189,8 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i fieldNameInput.isSingleLine = true AlertDialog.Builder(this).show { customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) - title(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_add) - positiveButton(_root_ide_package_.com.ichi2.anki.R.string.menu_add) { + title(R.string.model_field_editor_add) + positiveButton(R.string.menu_add) { // Name is valid, now field is added val fieldName = uniqueName(fieldNameInput) try { @@ -220,7 +200,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i // Create dialogue to for schema change val c = ConfirmationDialog() - c.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) + c.setArgs(resources.getString(R.string.full_sync_confirmation)) val confirm = Runnable { try { @@ -233,10 +213,10 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i c.setConfirm(confirm) this@NoteTypeFieldEditor.showDialogFragment(c) } - getColUnsafe.notetypes.update(notetype) + getColUnsafe.notetypes.update(viewModel.state.value.notetype) initialize() } - negativeButton(_root_ide_package_.com.ichi2.anki.R.string.dialog_cancel) + negativeButton(R.string.dialog_cancel) } } } @@ -256,15 +236,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i } else { getColUnsafe.modSchema(check = false) } - launchCatchingTask { - Timber.d("doInBackgroundAddField") - withProgress { - withCol { - notetypes.addFieldModChanged(notetype, notetypes.newField(fieldName)) - } - } - initialize() - } + viewModel.add(fieldName) } /* @@ -283,20 +255,22 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i ) } - if (fieldsLabels.size < 2) { + if (viewModel.state.value.fieldsLabels.size < 2) { _root_ide_package_.com.ichi2.anki.showThemedToast( this, - resources.getString(_root_ide_package_.com.ichi2.anki.R.string.toast_last_field), + resources.getString(R.string.toast_last_field), true, ) } else { try { getColUnsafe.modSchema(check = true) - val fieldName = noteFields[currentPos].name + val fieldName = + viewModel.state.value.noteFields[viewModel.state.value.currentPos] + .name ConfirmationDialog().let { it.setArgs( title = fieldName, - message = resources.getString(_root_ide_package_.com.ichi2.anki.R.string.field_delete_warning), + message = resources.getString(R.string.field_delete_warning), ) it.setConfirm(confirm) showDialogFragment(it) @@ -305,7 +279,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i e.log() ConfirmationDialog().let { it.setConfirm(confirm) - it.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) + it.setArgs(resources.getString(R.string.full_sync_confirmation)) showDialogFragment(it) } } @@ -313,26 +287,8 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i } private fun deleteField() { - launchCatchingTask { - Timber.d("doInBackGroundDeleteField") - withProgress(message = getString(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_changing)) { - val result = - withCol { - try { - notetypes.remFieldLegacy(notetype, noteFields[currentPos]) - true - } catch (e: ConfirmModSchemaException) { - // Should never be reached - e.log() - false - } - } - if (!result) { - closeActivity() - } - initialize() - } - } + val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] + viewModel.delete(field) } /* @@ -343,12 +299,12 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i fieldNameInput = FixedEditText(this).apply { focusWithKeyboard() } fieldNameInput?.let { fieldNameInput -> fieldNameInput.isSingleLine = true - fieldNameInput.setText(fieldsLabels[currentPos]) + fieldNameInput.setText(viewModel.state.value.fieldsLabels[viewModel.state.value.currentPos]) fieldNameInput.moveCursorToEnd() AlertDialog.Builder(this).show { customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) - title(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_rename) - positiveButton(_root_ide_package_.com.ichi2.anki.R.string.rename) { + title(R.string.model_field_editor_rename) + positiveButton(R.string.rename) { if (uniqueName(fieldNameInput) == null) { return@positiveButton } @@ -360,7 +316,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i // Handler mod schema confirmation val c = ConfirmationDialog() - c.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) + c.setArgs(resources.getString(R.string.full_sync_confirmation)) val confirm = Runnable { getColUnsafe.modSchema(check = false) @@ -375,7 +331,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i this@NoteTypeFieldEditor.showDialogFragment(c) } } - negativeButton(_root_ide_package_.com.ichi2.anki.R.string.dialog_cancel) + negativeButton(R.string.dialog_cancel) } } } @@ -397,15 +353,15 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i AlertDialog .Builder(this) .show { - positiveButton(_root_ide_package_.com.ichi2.anki.R.string.dialog_ok) { + positiveButton(R.string.dialog_ok) { val input = (it as AlertDialog).getInputField() result(input.text.toString().toInt()) } - negativeButton(_root_ide_package_.com.ichi2.anki.R.string.dialog_cancel) + negativeButton(R.string.dialog_cancel) setMessage(TR.fieldsNewPosition1(numberOfTemplates)) - setView(_root_ide_package_.com.ichi2.anki.R.layout.dialog_generic_text_input) + setView(R.layout.dialog_generic_text_input) }.input( - prefill = (currentPos + 1).toString(), + prefill = (viewModel.state.value.currentPos + 1).toString(), inputType = InputType.TYPE_CLASS_NUMBER, displayKeyboard = true, waitForPositiveButton = false, @@ -416,10 +372,10 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i } // handle repositioning - showDialog(fieldsLabels.size) { newPosition -> - if (newPosition == currentPos + 1) return@showDialog + showDialog(viewModel.state.value.fieldsLabels.size) { newPosition -> + if (newPosition == viewModel.state.value.currentPos + 1) return@showDialog - Timber.i("Repositioning field from %d to %d", currentPos, newPosition) + Timber.i("Repositioning field from %d to %d", viewModel.state.value.currentPos, newPosition) try { getColUnsafe.modSchema(check = true) repositionField(newPosition - 1) @@ -428,7 +384,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i // Handle mod schema confirmation val c = ConfirmationDialog() - c.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) + c.setArgs(resources.getString(R.string.full_sync_confirmation)) val confirm = Runnable { try { @@ -445,26 +401,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i } private fun repositionField(index: Int) { - launchCatchingTask { - withProgress(message = getString(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_changing)) { - val result = - withCol { - Timber.d("doInBackgroundRepositionField") - try { - notetypes.moveFieldLegacy(notetype, noteFields[currentPos], index) - true - } catch (e: ConfirmModSchemaException) { - e.log() - // Should never be reached - false - } - } - if (!result) { - closeActivity() - } - initialize() - } - } + viewModel.reposition(viewModel.state.value.noteFields[viewModel.state.value.currentPos], index) } /* @@ -472,14 +409,9 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i */ @Throws(ConfirmModSchemaException::class) private fun renameField() { - val fieldLabel = - fieldNameInput!! - .text - .toString() - .replace("[\\n\\r]".toRegex(), "") - val field = noteFields[currentPos] - getColUnsafe.notetypes.renameFieldLegacy(notetype, field, fieldLabel) - initialize() + val fieldLabel = uniqueName(fieldNameInput!!) ?: return + val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] + viewModel.rename(field, fieldLabel) } /* @@ -488,38 +420,24 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i private fun sortByField() { try { getColUnsafe.modSchema(check = true) - launchCatchingTask { changeSortField(notetype, currentPos) } + launchCatchingTask { changeSortField(viewModel.state.value.currentPos) } } catch (e: ConfirmModSchemaException) { e.log() // Handler mMod schema confirmation val c = ConfirmationDialog() - c.setArgs(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.full_sync_confirmation)) + c.setArgs(resources.getString(R.string.full_sync_confirmation)) val confirm = Runnable { getColUnsafe.modSchema(check = false) - launchCatchingTask { changeSortField(notetype, currentPos) } + launchCatchingTask { changeSortField(viewModel.state.value.currentPos) } } c.setConfirm(confirm) this@NoteTypeFieldEditor.showDialogFragment(c) } } - private suspend fun changeSortField( - notetype: NotetypeJson, - idx: Int, - ) { - withProgress(resources.getString(_root_ide_package_.com.ichi2.anki.R.string.model_field_editor_changing)) { - withCol { - Timber.d("doInBackgroundChangeSortField") - notetypes.setSortIndex(notetype, idx) - notetypes.save(notetype) - } - } - initialize() - } - - private fun closeActivity() { - finish() + private suspend fun changeSortField(idx: Int) { + viewModel.changeSort(idx) } fun handleAction(contextMenuAction: NoteTypeFieldEditorContextMenuAction) { @@ -543,10 +461,11 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i * This allows some keyboard (GBoard) to change language */ private fun addFieldLocaleHint(selectedLocale: Locale) { - setLanguageHintForField(getColUnsafe.notetypes, notetype, currentPos, selectedLocale) + val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] + viewModel.languageHint(field, selectedLocale) val format = getString( - _root_ide_package_.com.ichi2.anki.R.string.model_field_editor_language_hint_dialog_success_result, + R.string.model_field_editor_language_hint_dialog_success_result, selectedLocale.displayName, ) showSnackbar(format, Snackbar.LENGTH_SHORT) @@ -572,10 +491,10 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(_root_ide_package_.com.i * So far the only kind is SORT, which defines the field upon which notes could be sorted */ private fun fieldNamesWithKind(): List> = - fieldsLabels.mapIndexed { index, fieldName -> + viewModel.state.value.fieldsLabels.mapIndexed { index, fieldName -> Pair( fieldName, - if (index == notetype.sortf) NodetypeKind.SORT else NodetypeKind.UNDEFINED, + if (index == viewModel.state.value.notetype.sortf) NodetypeKind.SORT else NodetypeKind.UNDEFINED, ) } @@ -612,7 +531,7 @@ internal class NoteFieldAdapter( binding.fieldName.setCompoundDrawablesRelativeWithIntrinsicBoundsKt( end = when (kind) { - NodetypeKind.SORT -> _root_ide_package_.com.ichi2.anki.R.drawable.ic_sort + NodetypeKind.SORT -> R.drawable.ic_sort NodetypeKind.UNDEFINED -> 0 }, ) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt new file mode 100644 index 000000000000..e4004cc01578 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt @@ -0,0 +1,14 @@ +package com.ichi2.anki.notetype.fieldeditor + +import com.ichi2.anki.libanki.Fields +import com.ichi2.anki.libanki.NoteTypeId +import com.ichi2.anki.libanki.NotetypeJson + +data class NoteTypeFieldEditorState( + val notetype: NotetypeJson, + val noteTypeId: NoteTypeId = 0, + val currentPos: Int = 0, + val noteFields: Fields = notetype.fields, + val fieldsLabels: List = notetype.fieldsNames, + val isLoading: Boolean = false, +) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt new file mode 100644 index 000000000000..9255359c816f --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -0,0 +1,145 @@ +package com.ichi2.anki.notetype.fieldeditor + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ichi2.anki.CollectionManager.getColUnsafe +import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.anki.libanki.Field +import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor.Companion.EXTRA_NOTETYPE_ID +import com.ichi2.anki.servicelayer.LanguageHint +import com.ichi2.anki.servicelayer.LanguageHintService.languageHint +import com.ichi2.anki.servicelayer.LanguageHintService.setLanguageHintForField +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber + +class NoteTypeFieldEditorViewModel( + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val _state by lazy { + val noteTypeID = savedStateHandle.get(EXTRA_NOTETYPE_ID) ?: 0 + val notetype = getColUnsafe().notetypes.get(noteTypeID)!! + MutableStateFlow( + NoteTypeFieldEditorState( + notetype = notetype, + noteTypeId = noteTypeID, + ), + ) + } + val state: StateFlow = _state.asStateFlow() + + fun initialize() { + viewModelScope.launch { + refreshNoteTypes() + } + } + + fun updateCurrentPosition(position: Int) { + _state.update { oldState -> oldState.copy(currentPos = position) } + } + + fun add(name: String) { + Timber.d("doInBackgroundAddField") + _state.update { oldState -> oldState.copy(isLoading = true) } + viewModelScope.launch { + runCatching { + withCol { + val notetype = notetypes.get(state.value.noteTypeId)!! + notetypes.addField(notetype, notetypes.newField(name)) + notetypes.save(notetype) + } + } + initialize() + } + } + + fun rename( + field: Field, + name: String, + ) { + Timber.d("doInBackgroundRenameField") + _state.update { oldState -> oldState.copy(isLoading = true) } + viewModelScope.launch { + runCatching { + withCol { + val notetype = notetypes.get(state.value.noteTypeId)!! + notetypes.renameField(notetype, field, name) + notetypes.save(notetype) + } + } + initialize() + } + } + + fun delete(field: Field) { + Timber.d("doInBackGroundDeleteField") + _state.update { oldState -> oldState.copy(isLoading = true) } + viewModelScope.launch { + runCatching { + withCol { + val notetype = notetypes.get(state.value.noteTypeId)!! + notetypes.removeField(notetype, field) + notetypes.save(notetype) + } + } + initialize() + } + } + + fun changeSort(position: Int) { + Timber.d("doInBackgroundChangeSortField") + viewModelScope.launch { + runCatching { + withCol { + val notetype = notetypes.get(state.value.noteTypeId)!! + notetypes.setSortIndex(notetype, position) + notetypes.save(notetype) + } + } + initialize() + } + } + + fun reposition( + field: Field, + position: Int, + ) { + Timber.d("doInBackgroundRepositionField") + viewModelScope.launch { + runCatching { + withCol { + val notetype = notetypes.get(state.value.noteTypeId)!! + notetypes.repositionField(notetype, field, position) + } + } + initialize() + } + } + + fun languageHint( + field: Field, + locale: LanguageHint, + ) { + field.languageHint + viewModelScope.launch { + runCatching { + withCol { + setLanguageHintForField(notetypes, state.value.notetype, state.value.currentPos, locale) + } + } + initialize() + } + } + + fun undo() { + } + + suspend fun refreshNoteTypes() { + val notetype = withCol { notetypes.get(state.value.noteTypeId)!! } + _state.update { oldState -> oldState.copy(isLoading = false, notetype = notetype) } + } +} From 9a336d6752eedbb67e28d88486e9f6c68e45f2cf Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sat, 7 Mar 2026 20:08:32 +0900 Subject: [PATCH 03/19] refactor(notetypefieldeditor): split edit field dialogs into different files --- .../fieldeditor/AddNewNoteTypeField.kt | 43 +++ .../fieldeditor/NoteTypeFieldEditor.kt | 311 ++++-------------- .../fieldeditor/RenameNoteTypeField.kt | 47 +++ .../fieldeditor/RepositionTypeField.kt | 48 +++ .../com/ichi2/anki/NoteTypeFieldEditorTest.kt | 8 +- 5 files changed, 207 insertions(+), 250 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RenameNoteTypeField.kt create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RepositionTypeField.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt new file mode 100644 index 000000000000..4a70feff676a --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt @@ -0,0 +1,43 @@ +package com.ichi2.anki.notetype.fieldeditor + +import androidx.appcompat.app.AlertDialog +import com.ichi2.anki.R +import com.ichi2.anki.launchCatchingTask +import com.ichi2.anki.sync.userAcceptsSchemaChange +import com.ichi2.ui.FixedEditText +import com.ichi2.utils.customView +import com.ichi2.utils.negativeButton +import com.ichi2.utils.positiveButton +import com.ichi2.utils.show +import com.ichi2.utils.title + +class AddNewNoteTypeField( + private val activity: NoteTypeFieldEditor, +) { + fun showAddNewNoteTypeFieldDialog(confirm: (String) -> Unit) { + val fieldNameInput = + FixedEditText(activity).apply { + focusWithKeyboard() + isSingleLine = true + } + + activity.apply { + launchCatchingTask { + val confirmation = userAcceptsSchemaChange() + if (!confirmation) { + return@launchCatchingTask + } + AlertDialog + .Builder(activity) + .show { + customView(fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) + title(R.string.model_field_editor_add) + positiveButton(R.string.menu_add) { + confirm(fieldNameInput.text.toString()) + } + negativeButton(R.string.dialog_cancel) + } + } + } + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index cc599f1f5593..6a5eed8ca40f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -18,22 +18,18 @@ package com.ichi2.anki.notetype.fieldeditor import android.content.Context import android.os.Bundle -import android.text.InputType import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView -import android.widget.EditText import androidx.activity.viewModels import androidx.annotation.VisibleForTesting -import androidx.appcompat.app.AlertDialog import androidx.core.os.BundleCompat import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.snackbar.Snackbar -import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest import com.ichi2.anki.databinding.ItemNotetypeFieldBinding @@ -47,23 +43,15 @@ import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.NoteTypeFieldEditor import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.libanki.Collection import com.ichi2.anki.libanki.exception.ConfirmModSchemaException +import com.ichi2.anki.showThemedToast import com.ichi2.anki.snackbar.showSnackbar +import com.ichi2.anki.sync.userAcceptsSchemaChange import com.ichi2.anki.utils.ext.dismissAllDialogFragments import com.ichi2.anki.utils.ext.setCompoundDrawablesRelativeWithIntrinsicBoundsKt import com.ichi2.anki.utils.ext.setFragmentResultListener import com.ichi2.anki.utils.ext.showDialogFragment -import com.ichi2.ui.FixedEditText -import com.ichi2.utils.customView -import com.ichi2.utils.getInputField -import com.ichi2.utils.input -import com.ichi2.utils.moveCursorToEnd -import com.ichi2.utils.negativeButton -import com.ichi2.utils.positiveButton -import com.ichi2.utils.show -import com.ichi2.utils.title import dev.androidbroadcast.vbpd.viewBinding import kotlinx.coroutines.launch -import org.json.JSONException import timber.log.Timber import java.util.Locale @@ -72,9 +60,6 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field private val binding by viewBinding(NoteTypeFieldEditorBinding::bind) val viewModel by viewModels() - // Position of the current field selected - private var fieldNameInput: EditText? = null - // ---------------------------------------------------------------------------- // ANDROID METHODS // ---------------------------------------------------------------------------- @@ -84,7 +69,6 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field } super.onCreate(savedInstanceState) setContentView(R.layout.note_type_field_editor) - enableToolbar() binding.notetypeName.text = intent.getStringExtra(EXTRA_NOTETYPE_NAME) startLoadingCollection() setFragmentResultListener(REQUEST_HINT_LOCALE_SELECTION) { _, bundle -> @@ -142,10 +126,9 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field * @param fieldNameInput Editor to get the input * @return The value to use, or null in case of failure */ - private fun uniqueName(fieldNameInput: EditText): String? { + private fun uniqueName(name: String): String? { var input = - fieldNameInput.text - .toString() + name .replace("[\\n\\r{}:\"]".toRegex(), "") // The number of #, ^, /, space, tab, starting the input var offset = 0 @@ -157,7 +140,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field } input = input.substring(offset).trim() if (input.isEmpty()) { - _root_ide_package_.com.ichi2.anki.showThemedToast( + showThemedToast( this, resources.getString(R.string.toast_empty_name), true, @@ -167,7 +150,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field if (viewModel.state.value.fieldsLabels .any { input == it } ) { - _root_ide_package_.com.ichi2.anki.showThemedToast( + showThemedToast( this, resources.getString(R.string.toast_duplicate_field), true, @@ -181,157 +164,71 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field * Creates a dialog to create a field */ private fun addFieldDialog() { - fieldNameInput = - FixedEditText(this).apply { - focusWithKeyboard() - } - fieldNameInput?.let { fieldNameInput -> - fieldNameInput.isSingleLine = true - AlertDialog.Builder(this).show { - customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) - title(R.string.model_field_editor_add) - positiveButton(R.string.menu_add) { - // Name is valid, now field is added - val fieldName = uniqueName(fieldNameInput) - try { - addField(fieldName, true) - } catch (e: ConfirmModSchemaException) { - e.log() - - // Create dialogue to for schema change - val c = ConfirmationDialog() - c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = - Runnable { - try { - addField(fieldName, false) - } catch (e1: ConfirmModSchemaException) { - e1.log() - // This should never be thrown - } - } - c.setConfirm(confirm) - this@NoteTypeFieldEditor.showDialogFragment(c) - } - getColUnsafe.notetypes.update(viewModel.state.value.notetype) - initialize() - } - negativeButton(R.string.dialog_cancel) + val addFieldDialog = AddNewNoteTypeField(this) + addFieldDialog.showAddNewNoteTypeFieldDialog { name -> + launchCatchingTask { + val validName = uniqueName(name) + val isConfirmed = userAcceptsSchemaChange() + if (!isConfirmed) return@launchCatchingTask + viewModel.add(name) } } } - /** - * Adds a field with the given name - */ - @Throws(ConfirmModSchemaException::class) - private fun addField( - fieldName: String?, - modSchemaCheck: Boolean, - ) { - fieldName ?: return - // Name is valid, now field is added - if (modSchemaCheck) { - getColUnsafe.modSchema(check = true) - } else { - getColUnsafe.modSchema(check = false) - } - viewModel.add(fieldName) - } - /* * Creates a dialog to delete the currently selected field */ private fun deleteFieldDialog() { - val confirm = - Runnable { - getColUnsafe.modSchema(check = false) - deleteField() - - // This ensures that the context menu closes after the field has been deleted - supportFragmentManager.popBackStackImmediate( - null, - FragmentManager.POP_BACK_STACK_INCLUSIVE, - ) - } - if (viewModel.state.value.fieldsLabels.size < 2) { - _root_ide_package_.com.ichi2.anki.showThemedToast( + showThemedToast( this, resources.getString(R.string.toast_last_field), true, ) - } else { - try { - getColUnsafe.modSchema(check = true) - val fieldName = - viewModel.state.value.noteFields[viewModel.state.value.currentPos] - .name - ConfirmationDialog().let { - it.setArgs( - title = fieldName, - message = resources.getString(R.string.field_delete_warning), + return + } + + val fieldName = + viewModel.state.value.noteFields[viewModel.state.value.currentPos] + .name + ConfirmationDialog().let { + it.setArgs( + title = fieldName, + message = resources.getString(R.string.field_delete_warning), + ) + it.setConfirm { + launchCatchingTask { + val isConfirmed = userAcceptsSchemaChange() + if (!isConfirmed) return@launchCatchingTask + + val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] + viewModel.delete(field) + + // This ensures that the context menu closes after the field has been deleted + supportFragmentManager.popBackStackImmediate( + null, + FragmentManager.POP_BACK_STACK_INCLUSIVE, ) - it.setConfirm(confirm) - showDialogFragment(it) - } - } catch (e: ConfirmModSchemaException) { - e.log() - ConfirmationDialog().let { - it.setConfirm(confirm) - it.setArgs(resources.getString(R.string.full_sync_confirmation)) - showDialogFragment(it) } } + showDialogFragment(it) } } - private fun deleteField() { - val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] - viewModel.delete(field) - } - /* * Creates a dialog to rename the currently selected field * Processing time is constant */ private fun renameFieldDialog() { - fieldNameInput = FixedEditText(this).apply { focusWithKeyboard() } - fieldNameInput?.let { fieldNameInput -> - fieldNameInput.isSingleLine = true - fieldNameInput.setText(viewModel.state.value.fieldsLabels[viewModel.state.value.currentPos]) - fieldNameInput.moveCursorToEnd() - AlertDialog.Builder(this).show { - customView(view = fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) - title(R.string.model_field_editor_rename) - positiveButton(R.string.rename) { - if (uniqueName(fieldNameInput) == null) { - return@positiveButton - } - // Field is valid, now rename - try { - renameField() - } catch (e: ConfirmModSchemaException) { - e.log() - - // Handler mod schema confirmation - val c = ConfirmationDialog() - c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = - Runnable { - getColUnsafe.modSchema(check = false) - try { - renameField() - } catch (e1: ConfirmModSchemaException) { - e1.log() - // This should never be thrown - } - } - c.setConfirm(confirm) - this@NoteTypeFieldEditor.showDialogFragment(c) - } - } - negativeButton(R.string.dialog_cancel) + val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] + val renameFieldDialog = RenameNoteTypeField(this@NoteTypeFieldEditor, field.name) + renameFieldDialog.showRenameNoteTypeFieldDialog { name -> + val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] + launchCatchingTask { + val validName = uniqueName(name) + val isConfirmed = userAcceptsSchemaChange() + if (!isConfirmed) return@launchCatchingTask + viewModel.rename(field, name) } } } @@ -340,106 +237,29 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field * Displays a dialog to allow the user to reposition a field within a list. */ private fun repositionFieldDialog() { - /** - * Shows an input dialog for selecting a new position. - * - * @param numberOfTemplates The total number of available positions. - * @param result A lambda function that receives the validated new position as an integer. - */ - fun showDialog( - numberOfTemplates: Int, - result: (Int) -> Unit, - ) { - AlertDialog - .Builder(this) - .show { - positiveButton(R.string.dialog_ok) { - val input = (it as AlertDialog).getInputField() - result(input.text.toString().toInt()) - } - negativeButton(R.string.dialog_cancel) - setMessage(TR.fieldsNewPosition1(numberOfTemplates)) - setView(R.layout.dialog_generic_text_input) - }.input( - prefill = (viewModel.state.value.currentPos + 1).toString(), - inputType = InputType.TYPE_CLASS_NUMBER, - displayKeyboard = true, - waitForPositiveButton = false, - ) { dialog, text: CharSequence -> - val number = text.toString().toIntOrNull() - dialog.positiveButton.isEnabled = number != null && number in 1..numberOfTemplates - } - } - - // handle repositioning - showDialog(viewModel.state.value.fieldsLabels.size) { newPosition -> - if (newPosition == viewModel.state.value.currentPos + 1) return@showDialog - - Timber.i("Repositioning field from %d to %d", viewModel.state.value.currentPos, newPosition) - try { - getColUnsafe.modSchema(check = true) - repositionField(newPosition - 1) - } catch (e: ConfirmModSchemaException) { - e.log() - - // Handle mod schema confirmation - val c = ConfirmationDialog() - c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = - Runnable { - try { - getColUnsafe.modSchema(check = false) - repositionField(newPosition - 1) - } catch (e1: JSONException) { - throw RuntimeException(e1) - } - } - c.setConfirm(confirm) - this@NoteTypeFieldEditor.showDialogFragment(c) + val repositionFieldDialog = RepositionTypeField(this@NoteTypeFieldEditor, viewModel.state.value.fieldsLabels.size) + repositionFieldDialog.showRepositionNoteTypeFieldDialog { position -> + launchCatchingTask { + val isConfirmed = userAcceptsSchemaChange() + if (!isConfirmed) return@launchCatchingTask + Timber.i("Repositioning field from %d to %d", viewModel.state.value.currentPos, position) + val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] + viewModel.reposition(field, position) } } } - private fun repositionField(index: Int) { - viewModel.reposition(viewModel.state.value.noteFields[viewModel.state.value.currentPos], index) - } - - /* - * Renames the current field - */ - @Throws(ConfirmModSchemaException::class) - private fun renameField() { - val fieldLabel = uniqueName(fieldNameInput!!) ?: return - val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] - viewModel.rename(field, fieldLabel) - } - /* * Changes the sort field (that displays in card browser) to the current field */ private fun sortByField() { - try { - getColUnsafe.modSchema(check = true) - launchCatchingTask { changeSortField(viewModel.state.value.currentPos) } - } catch (e: ConfirmModSchemaException) { - e.log() - // Handler mMod schema confirmation - val c = ConfirmationDialog() - c.setArgs(resources.getString(R.string.full_sync_confirmation)) - val confirm = - Runnable { - getColUnsafe.modSchema(check = false) - launchCatchingTask { changeSortField(viewModel.state.value.currentPos) } - } - c.setConfirm(confirm) - this@NoteTypeFieldEditor.showDialogFragment(c) + launchCatchingTask { + val isConfirmed = userAcceptsSchemaChange() + if (!isConfirmed) return@launchCatchingTask + viewModel.changeSort(viewModel.state.value.currentPos) } } - private suspend fun changeSortField(idx: Int) { - viewModel.changeSort(idx) - } - fun handleAction(contextMenuAction: NoteTypeFieldEditorContextMenuAction) { when (contextMenuAction) { NoteTypeFieldEditorContextMenuAction.Sort -> sortByField() @@ -474,16 +294,17 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field @VisibleForTesting(otherwise = VisibleForTesting.NONE) @Throws(ConfirmModSchemaException::class) - fun addField(fieldNameInput: EditText) { - val fieldName = uniqueName(fieldNameInput) - addField(fieldName, true) + fun addField(name: String) { + val fieldName = uniqueName(name) ?: return + viewModel.add(fieldName) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) @Throws(ConfirmModSchemaException::class) - fun renameField(fieldNameInput: EditText?) { - this.fieldNameInput = fieldNameInput - renameField() + fun renameField(name: String) { + val fieldLabel = uniqueName(name) ?: return + val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] + viewModel.rename(field, fieldLabel) } /* diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RenameNoteTypeField.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RenameNoteTypeField.kt new file mode 100644 index 000000000000..71a32b11977b --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RenameNoteTypeField.kt @@ -0,0 +1,47 @@ +package com.ichi2.anki.notetype.fieldeditor + +import androidx.appcompat.app.AlertDialog +import com.ichi2.anki.R +import com.ichi2.anki.launchCatchingTask +import com.ichi2.anki.sync.userAcceptsSchemaChange +import com.ichi2.ui.FixedEditText +import com.ichi2.utils.customView +import com.ichi2.utils.moveCursorToEnd +import com.ichi2.utils.negativeButton +import com.ichi2.utils.positiveButton +import com.ichi2.utils.show +import com.ichi2.utils.title + +class RenameNoteTypeField( + private val activity: NoteTypeFieldEditor, + val fieldName: String, +) { + fun showRenameNoteTypeFieldDialog(confirm: (name: String) -> Unit) { + val fieldNameInput = + FixedEditText(activity).apply { + focusWithKeyboard() + isSingleLine = true + setText(fieldName) + moveCursorToEnd() + } + + activity.apply { + launchCatchingTask { + val confirmation = userAcceptsSchemaChange() + if (!confirmation) { + return@launchCatchingTask + } + AlertDialog + .Builder(activity) + .show { + customView(fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) + title(R.string.model_field_editor_rename) + positiveButton(R.string.rename) { + confirm(fieldNameInput.text.toString()) + } + negativeButton(R.string.dialog_cancel) + } + } + } + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RepositionTypeField.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RepositionTypeField.kt new file mode 100644 index 000000000000..dd02ab0ec2cb --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RepositionTypeField.kt @@ -0,0 +1,48 @@ +package com.ichi2.anki.notetype.fieldeditor + +import android.text.InputType +import androidx.appcompat.app.AlertDialog +import com.ichi2.anki.CollectionManager.TR +import com.ichi2.anki.R +import com.ichi2.anki.launchCatchingTask +import com.ichi2.anki.sync.userAcceptsSchemaChange +import com.ichi2.utils.getInputField +import com.ichi2.utils.input +import com.ichi2.utils.negativeButton +import com.ichi2.utils.positiveButton +import com.ichi2.utils.show + +class RepositionTypeField( + private val activity: NoteTypeFieldEditor, + private val numberOfTemplates: Int, +) { + fun showRepositionNoteTypeFieldDialog(confirm: (position: Int) -> Unit) { + activity.apply { + launchCatchingTask { + val confirmation = userAcceptsSchemaChange() + if (!confirmation) { + return@launchCatchingTask + } + AlertDialog + .Builder(activity) + .show { + positiveButton(R.string.dialog_ok) { + val input = (it as AlertDialog).getInputField() + confirm(input.text.toString().toInt() - 1) + } + negativeButton(R.string.dialog_cancel) + setMessage(TR.fieldsNewPosition1(numberOfTemplates)) + setView(R.layout.dialog_generic_text_input) + }.input( + prefill = (numberOfTemplates + 1).toString(), + inputType = InputType.TYPE_CLASS_NUMBER, + displayKeyboard = true, + waitForPositiveButton = false, + ) { dialog, text: CharSequence -> + val number = text.toString().toIntOrNull() + dialog.positiveButton.isEnabled = number != null && number in 1..(numberOfTemplates + 1) + } + } + } + } +} diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt index a8b62cdf43e5..074d565bcc21 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt @@ -105,6 +105,7 @@ class NoteTypeFieldEditorTest( positiveButton(text = "") { try { val noteTypeName = "Basic" + val fieldName = fieldNameInput.text.toString() // start ModelFieldEditor activity val intent = Intent() @@ -123,11 +124,8 @@ class NoteTypeFieldEditorTest( intent, ) when (fieldOperationType) { - FieldOperationType.ADD_FIELD -> noteTypeFieldEditor.addField(fieldNameInput) - FieldOperationType.RENAME_FIELD -> - noteTypeFieldEditor.renameField( - fieldNameInput, - ) + FieldOperationType.ADD_FIELD -> noteTypeFieldEditor.addField(fieldName) + FieldOperationType.RENAME_FIELD -> noteTypeFieldEditor.renameField(fieldName) } } catch (exception: ConfirmModSchemaException) { throw RuntimeException(exception) From b7fbb953ffcf22ed167a71862a3ca68ebdf112c4 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:41:43 +0900 Subject: [PATCH 04/19] feat(notetypefieldeditor): edit notetypeField by moving list directly Migrated from listview to recyclerview and Made it pssible to drag and drop or swipe to edit notetypeField, and edit field names directly. Rmoved menu buttons. Show the current language settings. --- .../NoteTypeFieldEditorContextMenuTest.kt | 52 -- .../anki/dialogs/LocaleSelectionDialog.kt | 153 +++--- .../dialogs/NoteTypeFieldEditorContextMenu.kt | 56 --- .../fieldeditor/AddNewNoteTypeField.kt | 5 - .../fieldeditor/NoteTypeFieldEditor.kt | 453 ++++++++++++------ .../fieldeditor/NoteTypeFieldEditorState.kt | 11 +- .../NoteTypeFieldEditorViewModel.kt | 233 +++++++-- .../fieldeditor/NoteTypeFieldRowData.kt | 11 + .../fieldeditor/RenameNoteTypeField.kt | 47 -- .../anki/servicelayer/LanguageHintService.kt | 17 + .../res/color/edit_field_icon_button_tint.xml | 5 + .../src/main/res/drawable/field_check.xml | 11 + .../main/res/drawable/ic_edit_keyboard.xml | 11 + .../layout/item_locale_dialog_fragment.xml | 53 ++ .../main/res/layout/item_notetype_field.xml | 67 ++- .../locale_dialog_fragment_textview.xml | 29 -- .../res/layout/locale_selection_dialog.xml | 2 +- .../res/layout/note_type_field_editor.xml | 20 +- .../src/main/res/values/17-model-manager.xml | 1 + .../com/ichi2/anki/NoteTypeFieldEditorTest.kt | 3 +- 20 files changed, 752 insertions(+), 488 deletions(-) delete mode 100644 AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenuTest.kt delete mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenu.kt create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt delete mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RenameNoteTypeField.kt create mode 100644 AnkiDroid/src/main/res/color/edit_field_icon_button_tint.xml create mode 100644 AnkiDroid/src/main/res/drawable/field_check.xml create mode 100644 AnkiDroid/src/main/res/drawable/ic_edit_keyboard.xml create mode 100644 AnkiDroid/src/main/res/layout/item_locale_dialog_fragment.xml delete mode 100644 AnkiDroid/src/main/res/layout/locale_dialog_fragment_textview.xml diff --git a/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenuTest.kt b/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenuTest.kt deleted file mode 100644 index e19489acf6c2..000000000000 --- a/AnkiDroid/src/androidTest/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenuTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2022 lukstbit - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ -package com.ichi2.anki.dialogs - -import androidx.core.os.bundleOf -import androidx.fragment.app.testing.launchFragment -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.RootMatchers.isDialog -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.ichi2.anki.R -import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.NoteTypeFieldEditorContextMenuAction -import com.ichi2.anki.tests.InstrumentedTest -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith - -/** Tests [NoteTypeFieldEditorContextMenu] */ -@RunWith(AndroidJUnit4::class) -class NoteTypeFieldEditorContextMenuTest : InstrumentedTest() { - private val testDialogTitle = "test editor title" - - @Test - @Ignore("flaky") - fun showsAllOptions() { - launchFragment( - fragmentArgs = bundleOf(NoteTypeFieldEditorContextMenu.KEY_LABEL to testDialogTitle), - themeResId = R.style.Theme_Light, - ) { NoteTypeFieldEditorContextMenu() } - onView(withText(testDialogTitle)) - .inRoot(isDialog()) - .check(matches(isDisplayed())) - NoteTypeFieldEditorContextMenuAction.entries.forEach { - onView(withText(it.actionTextId)).check(matches(isDisplayed())) - } - } -} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/LocaleSelectionDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/LocaleSelectionDialog.kt index e46b9f7eb4b6..4a4030d5f49a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/LocaleSelectionDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/LocaleSelectionDialog.kt @@ -17,26 +17,29 @@ package com.ichi2.anki.dialogs import android.app.Dialog import android.os.Bundle +import android.view.LayoutInflater import android.view.ViewGroup import android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE import android.view.inputmethod.EditorInfo -import android.widget.Filter -import android.widget.Filterable -import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar +import androidx.core.os.BundleCompat import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.ichi2.anki.R import com.ichi2.anki.analytics.AnalyticsDialogFragment +import com.ichi2.anki.databinding.ItemLocaleDialogFragmentBinding import com.ichi2.anki.databinding.LocaleSelectionDialogBinding import com.ichi2.anki.dialogs.LocaleSelectionDialog.LocaleListAdapter.TextViewHolder import com.ichi2.anki.servicelayer.LanguageHintService +import com.ichi2.anki.servicelayer.LanguageHintService.compareLanguage import com.ichi2.ui.AccessibleSearchView import com.ichi2.utils.DisplayUtils.resizeWhenSoftInputShown -import com.ichi2.utils.TypedFilter import com.ichi2.utils.cancelable import com.ichi2.utils.customView import com.ichi2.utils.show @@ -47,17 +50,28 @@ import java.util.Locale * Currently supported only by Gboard. * @see LanguageHintService */ -class LocaleSelectionDialog : AnalyticsDialogFragment() { +class LocaleSelectionDialog private constructor() : AnalyticsDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val localeAdapter = - LocaleListAdapter( - Locale.getAvailableLocales() + IPALanguage, - ::sendSelectionResult, + val selectedLocale: Locale? = + BundleCompat.getSerializable( + requireArguments(), + KEY_SELECTED_LOCALE, + Locale::class.java, ) + val fieldPosition = requireArguments().getInt(KEY_SELECTED_FIELD_POSITION) + + val localeAdapter = + LocaleListAdapter(selectedLocale) { + sendSelectionResult(fieldPosition, it) + } val binding = LocaleSelectionDialogBinding.inflate(layoutInflater) binding.localeDialogSelectionList.adapter = localeAdapter - binding.localeDialogSelectionToolbar.setupMenuWith(localeAdapter) + localeAdapter.submitList(createLocaleList(selectedLocale)) + binding.localeDialogSelectionToolbar.setupMenuWith( + localeAdapter, + Pair(fieldPosition, selectedLocale), + ) return AlertDialog.Builder(requireContext()).show { cancelable(true) customView(binding.root) @@ -74,9 +88,13 @@ class LocaleSelectionDialog : AnalyticsDialogFragment() { dialog.window?.clearFlags(FLAG_NOT_FOCUSABLE or FLAG_ALT_FOCUSABLE_IM) } - private fun Toolbar.setupMenuWith(adapter: LocaleListAdapter) { + private fun Toolbar.setupMenuWith( + adapter: LocaleListAdapter, + currentSelection: Pair, + ) { + val (fieldPosition, selectedLocale) = currentSelection inflateMenu(R.menu.locale_dialog_search_bar) - setNavigationOnClickListener { sendSelectionResult() } + setNavigationOnClickListener { sendSelectionResult(fieldPosition, null) } (menu.findItem(R.id.locale_dialog_action_search).actionView as AccessibleSearchView).apply { imeOptions = EditorInfo.IME_ACTION_DONE setOnQueryTextListener( @@ -84,7 +102,8 @@ class LocaleSelectionDialog : AnalyticsDialogFragment() { override fun onQueryTextSubmit(query: String): Boolean = false override fun onQueryTextChange(newText: String): Boolean { - adapter.filter.filter(newText) + val locales = createLocaleList(selectedLocale, newText) + adapter.submitList(locales) return false } }, @@ -92,69 +111,70 @@ class LocaleSelectionDialog : AnalyticsDialogFragment() { } } - private fun sendSelectionResult(locale: Locale? = null) { + private fun sendSelectionResult( + position: Int, + locale: Locale?, + ) { parentFragmentManager.setFragmentResult( REQUEST_HINT_LOCALE_SELECTION, - bundleOf(KEY_SELECTED_LOCALE to locale), + bundleOf( + KEY_SELECTED_FIELD_POSITION to position, + KEY_SELECTED_LOCALE to locale, + ), ) } - private inner class LocaleListAdapter( - private val locales: Array, - private val onLocaleSelected: (Locale) -> Unit, - ) : RecyclerView.Adapter(), - Filterable { - private val filteredLocales: MutableList = locales.toMutableList() + private fun createLocaleList( + selectedLanguage: Locale?, + text: String = "", + ): List { + var result = Locales.toList() + if (selectedLanguage != null) { + result = listOf(selectedLanguage) + + result.filter { locale -> + !compareLanguage(locale, selectedLanguage) + } + } + if (text.isNotEmpty()) { + val normalisedConstraint = text.lowercase(Locale.getDefault()) + result = + result.filter { + it.displayName.lowercase(Locale.getDefault()).contains(normalisedConstraint) + } + } + return result + } - inner class TextViewHolder( - val textView: TextView, - ) : RecyclerView.ViewHolder(textView) + private class LocaleListAdapter( + private val selectedLocale: Locale?, + private val onLocaleSelected: (Locale) -> Unit, + ) : ListAdapter(DIFF_CALLBACK) { + class TextViewHolder( + val binding: ItemLocaleDialogFragmentBinding, + ) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, ) = TextViewHolder( - layoutInflater - .inflate(R.layout.locale_dialog_fragment_textview, parent, false) as TextView, + ItemLocaleDialogFragmentBinding.inflate(LayoutInflater.from(parent.context), parent, false), ) override fun onBindViewHolder( holder: TextViewHolder, position: Int, ) { - val locale = filteredLocales[position] - holder.textView.text = locale.displayName - holder.textView.setOnClickListener { onLocaleSelected(locale) } - } - - override fun getItemCount(): Int = filteredLocales.size - - override fun getFilter(): Filter { - return object : TypedFilter({ locales.toList() }) { - override fun filterResults( - constraint: CharSequence, - items: List, - ): List { - val normalisedConstraint = constraint.toString().lowercase(Locale.getDefault()) - return items.filter { - it.displayName.lowercase(Locale.getDefault()).contains(normalisedConstraint) - } - } - - override fun publishResults( - constraint: CharSequence?, - results: List, - ) { - filteredLocales.clear() - filteredLocales.addAll(results) - notifyDataSetChanged() - } - } + val locale = getItem(position) + holder.binding.localeDialogFragmentTextView.text = locale.displayName + holder.binding.root.setOnClickListener { onLocaleSelected(locale) } + val isSelected = selectedLocale != null && compareLanguage(locale, selectedLocale) + holder.binding.localeDialogFragmentImageView.isVisible = isSelected } } companion object { const val REQUEST_HINT_LOCALE_SELECTION = "request_hint_locale_selection" + const val KEY_SELECTED_FIELD_POSITION = "key_selected_field_position" const val KEY_SELECTED_LOCALE = "key_selected_locale" /** @@ -165,5 +185,30 @@ class LocaleSelectionDialog : AnalyticsDialogFragment() { * See https://en.wikipedia.org/wiki/International_Phonetic_Alphabet#IETF_language_tags */ private val IPALanguage = Locale.Builder().setLanguageTag("und-fonipa").build() + private val Locales = Locale.getAvailableLocales() + IPALanguage + + private val DIFF_CALLBACK = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Locale, + newItem: Locale, + ) = compareLanguage(oldItem, newItem) + + override fun areContentsTheSame( + oldItem: Locale, + newItem: Locale, + ) = compareLanguage(oldItem, newItem) + } + + fun newInstance( + fieldPosition: Int, + locale: Locale?, + ) = LocaleSelectionDialog().apply { + arguments = + bundleOf( + KEY_SELECTED_FIELD_POSITION to fieldPosition, + KEY_SELECTED_LOCALE to locale, + ) + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenu.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenu.kt deleted file mode 100644 index cdb02b1d5194..000000000000 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/NoteTypeFieldEditorContextMenu.kt +++ /dev/null @@ -1,56 +0,0 @@ -//noinspection MissingCopyrightHeader #8659 - -package com.ichi2.anki.dialogs - -import android.annotation.SuppressLint -import android.app.Dialog -import android.os.Bundle -import androidx.annotation.StringRes -import androidx.annotation.VisibleForTesting -import androidx.appcompat.app.AlertDialog -import androidx.core.os.bundleOf -import com.ichi2.anki.R -import com.ichi2.anki.analytics.AnalyticsDialogFragment -import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor -import com.ichi2.utils.create -import timber.log.Timber - -/** - * [NoteTypeFieldEditor]'s context menu - */ -class NoteTypeFieldEditorContextMenu : AnalyticsDialogFragment() { - @SuppressLint("CheckResult") - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - super.onCreate(savedInstanceState) - val availableItems = NoteTypeFieldEditorContextMenuAction.entries.sortedBy { it.order } - - return AlertDialog.Builder(requireActivity()).create { - setTitle(requireArguments().getString(KEY_LABEL)) - setItems(availableItems.map { resources.getString(it.actionTextId) }.toTypedArray()) { _, index -> - (activity as? NoteTypeFieldEditor)?.run { handleAction(availableItems[index]) } - ?: Timber.e("ContextMenu used from outside of its target activity!") - } - } - } - - enum class NoteTypeFieldEditorContextMenuAction( - val order: Int, - @StringRes val actionTextId: Int, - ) { - Reposition(0, R.string.model_field_editor_reposition_menu), - Sort(1, R.string.model_field_editor_sort_field), - Rename(2, R.string.model_field_editor_rename), - Delete(3, R.string.model_field_editor_delete), - AddLanguageHint(4, R.string.model_field_editor_language_hint), - } - - companion object { - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - const val KEY_LABEL = "key_label" - - fun newInstance(label: String): NoteTypeFieldEditorContextMenu = - NoteTypeFieldEditorContextMenu().apply { - arguments = bundleOf(KEY_LABEL to label) - } - } -} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt index 4a70feff676a..9eddb6bf8c6e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt @@ -3,7 +3,6 @@ package com.ichi2.anki.notetype.fieldeditor import androidx.appcompat.app.AlertDialog import com.ichi2.anki.R import com.ichi2.anki.launchCatchingTask -import com.ichi2.anki.sync.userAcceptsSchemaChange import com.ichi2.ui.FixedEditText import com.ichi2.utils.customView import com.ichi2.utils.negativeButton @@ -23,10 +22,6 @@ class AddNewNoteTypeField( activity.apply { launchCatchingTask { - val confirmation = userAcceptsSchemaChange() - if (!confirmation) { - return@launchCatchingTask - } AlertDialog .Builder(activity) .show { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 6a5eed8ca40f..6ed8d5d361b9 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -16,12 +16,10 @@ */ package com.ichi2.anki.notetype.fieldeditor -import android.content.Context import android.os.Bundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.AdapterView +import android.view.inputmethod.EditorInfo import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.core.os.BundleCompat @@ -29,27 +27,33 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textfield.TextInputEditText import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest import com.ichi2.anki.databinding.ItemNotetypeFieldBinding import com.ichi2.anki.databinding.NoteTypeFieldEditorBinding import com.ichi2.anki.dialogs.ConfirmationDialog import com.ichi2.anki.dialogs.LocaleSelectionDialog +import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.KEY_SELECTED_FIELD_POSITION import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.KEY_SELECTED_LOCALE import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.REQUEST_HINT_LOCALE_SELECTION -import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.Companion.newInstance -import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.NoteTypeFieldEditorContextMenuAction import com.ichi2.anki.launchCatchingTask -import com.ichi2.anki.libanki.Collection import com.ichi2.anki.libanki.exception.ConfirmModSchemaException import com.ichi2.anki.showThemedToast import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.sync.userAcceptsSchemaChange import com.ichi2.anki.utils.ext.dismissAllDialogFragments -import com.ichi2.anki.utils.ext.setCompoundDrawablesRelativeWithIntrinsicBoundsKt +import com.ichi2.anki.utils.ext.getIntOrNull import com.ichi2.anki.utils.ext.setFragmentResultListener import com.ichi2.anki.utils.ext.showDialogFragment +import com.ichi2.anki.utils.hideKeyboard +import com.ichi2.utils.moveCursorToEnd import dev.androidbroadcast.vbpd.viewBinding import kotlinx.coroutines.launch import timber.log.Timber @@ -60,6 +64,113 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field private val binding by viewBinding(NoteTypeFieldEditorBinding::bind) val viewModel by viewModels() + private val adapter by lazy { + val listener = + object : NoteFieldAdapter.ItemChangeListener { + override fun onNameChanged( + position: Int, + name: String, + ) { + launchCatchingTask { + val validName = uniqueName(name) + val isValidName = validName != null + val isConfirmed = isValidName && userAcceptsSchemaChange() + if (isConfirmed) { + viewModel.rename(position, validName) + } else { + // clear temporary edittext changes + viewModel.forceRefresh(position) + } + } + } + + override fun onSortChanged(position: Int) { + launchCatchingTask { + val isConfirmed = userAcceptsSchemaChange() + if (!isConfirmed) return@launchCatchingTask + viewModel.changeSort(position) + } + } + + override fun onLocaleChangeRequested(position: Int) { + val locale = + viewModel.state.value.fields[position] + .locale + localeHintDialog(locale, position) + } + + override fun onDeleted(position: Int) { + deleteFieldDialog(position) + } + } + return@lazy NoteFieldAdapter(listener) + } + + val touchHelper by lazy { + val callback = + object : ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, + ) { + var dragFromPosition: Int = RecyclerView.NO_POSITION + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean { + viewModel.visuallyReposition(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + return true + } + + override fun onSwiped( + viewHolder: RecyclerView.ViewHolder, + direction: Int, + ) { + val position = viewHolder.bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + deleteFieldDialog(position) + } + } + + override fun onSelectedChanged( + viewHolder: RecyclerView.ViewHolder?, + actionState: Int, + ) { + super.onSelectedChanged(viewHolder, actionState) + + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + dragFromPosition = viewHolder?.bindingAdapterPosition ?: RecyclerView.NO_POSITION + } + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + ) { + val dragFromPosition = dragFromPosition + val dragToPosition = viewHolder.bindingAdapterPosition + + if (dragFromPosition != RecyclerView.NO_POSITION && dragToPosition != RecyclerView.NO_POSITION && + dragFromPosition != dragToPosition + ) { + launchCatchingTask { + val isConfirmed = userAcceptsSchemaChange() + if (isConfirmed) { + viewModel.reposition(dragFromPosition, dragToPosition) + } else { + // clear list order changes + viewModel.smartRefresh() + } + } + } + + this.dragFromPosition = RecyclerView.NO_POSITION + } + } + return@lazy ItemTouchHelper(callback) + } + // ---------------------------------------------------------------------------- // ANDROID METHODS // ---------------------------------------------------------------------------- @@ -69,8 +180,6 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field } super.onCreate(savedInstanceState) setContentView(R.layout.note_type_field_editor) - binding.notetypeName.text = intent.getStringExtra(EXTRA_NOTETYPE_NAME) - startLoadingCollection() setFragmentResultListener(REQUEST_HINT_LOCALE_SELECTION) { _, bundle -> val selectedLocale = BundleCompat.getSerializable( @@ -78,44 +187,30 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field KEY_SELECTED_LOCALE, Locale::class.java, ) - if (selectedLocale != null) { - addFieldLocaleHint(selectedLocale) + val fieldPosition = bundle.getIntOrNull(KEY_SELECTED_FIELD_POSITION) + if (fieldPosition != null) { + addFieldLocaleHint(fieldPosition, selectedLocale) } dismissAllDialogFragments() } - } - - // ---------------------------------------------------------------------------- - // ANKI METHODS - // ---------------------------------------------------------------------------- - override fun onCollectionLoaded(col: Collection) { - super.onCollectionLoaded(col) - initialize() - } - // ---------------------------------------------------------------------------- - // UI SETUP - // ---------------------------------------------------------------------------- + enableToolbar().apply { + subtitle = intent.getStringExtra(EXTRA_NOTETYPE_NAME) + } - /** - * Initialize the data holding properties and the UI from the model. This method expects that it - * isn't followed by other type of work that access the data properties as it has the capability - * to finish the activity. - */ - private fun initialize() { + binding.fields.apply { + layoutManager = LinearLayoutManager(this@NoteTypeFieldEditor) + adapter = this@NoteTypeFieldEditor.adapter + touchHelper.attachToRecyclerView(this@apply) + } + binding.btnAdd.setOnClickListener { addFieldDialog() } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { - binding.fields.adapter = NoteFieldAdapter(this@NoteTypeFieldEditor, fieldNamesWithKind()) + adapter.submitList(it.fields) } } } - binding.fields.onItemClickListener = - AdapterView.OnItemClickListener { _, _, position: Int, _ -> - showDialogFragment(newInstance(viewModel.state.value.fieldsLabels[position])) - viewModel.updateCurrentPosition(position) - } - binding.btnAdd.setOnClickListener { addFieldDialog() } } // ---------------------------------------------------------------------------- // CONTEXT MENU DIALOGUES @@ -123,7 +218,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field /** * Clean the input field or explain why it's rejected - * @param fieldNameInput Editor to get the input + * @param name the input * @return The value to use, or null in case of failure */ private fun uniqueName(name: String): String? { @@ -147,8 +242,8 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) return null } - if (viewModel.state.value.fieldsLabels - .any { input == it } + if (viewModel.state.value.fields + .any { input == it.name } ) { showThemedToast( this, @@ -168,18 +263,19 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field addFieldDialog.showAddNewNoteTypeFieldDialog { name -> launchCatchingTask { val validName = uniqueName(name) - val isConfirmed = userAcceptsSchemaChange() + val isConfirmed = validName != null && userAcceptsSchemaChange() if (!isConfirmed) return@launchCatchingTask - viewModel.add(name) + viewModel.add(validName) } } } - /* - * Creates a dialog to delete the currently selected field + /** + * Creates a dialog to delete the field + * @param position the position of the field */ - private fun deleteFieldDialog() { - if (viewModel.state.value.fieldsLabels.size < 2) { + private fun deleteFieldDialog(position: Int) { + if (viewModel.state.value.fields.size < 2) { showThemedToast( this, resources.getString(R.string.toast_last_field), @@ -189,7 +285,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field } val fieldName = - viewModel.state.value.noteFields[viewModel.state.value.currentPos] + viewModel.state.value.fields[position] .name ConfirmationDialog().let { it.setArgs( @@ -199,10 +295,11 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field it.setConfirm { launchCatchingTask { val isConfirmed = userAcceptsSchemaChange() - if (!isConfirmed) return@launchCatchingTask - - val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] - viewModel.delete(field) + if (isConfirmed) { + viewModel.delete(position) + } else { + viewModel.forceRefresh(position) + } // This ensures that the context menu closes after the field has been deleted supportFragmentManager.popBackStackImmediate( @@ -211,85 +308,48 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) } } - showDialogFragment(it) - } - } - - /* - * Creates a dialog to rename the currently selected field - * Processing time is constant - */ - private fun renameFieldDialog() { - val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] - val renameFieldDialog = RenameNoteTypeField(this@NoteTypeFieldEditor, field.name) - renameFieldDialog.showRenameNoteTypeFieldDialog { name -> - val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] - launchCatchingTask { - val validName = uniqueName(name) - val isConfirmed = userAcceptsSchemaChange() - if (!isConfirmed) return@launchCatchingTask - viewModel.rename(field, name) + it.setCancel { + viewModel.forceRefresh(position) } + it.isCancelable = false + showDialogFragment(it) } } /** - * Displays a dialog to allow the user to reposition a field within a list. + * Creates a dialog to show the available locale list for the field + * @param locale the current locale of the field + * @param position the position of the field */ - private fun repositionFieldDialog() { - val repositionFieldDialog = RepositionTypeField(this@NoteTypeFieldEditor, viewModel.state.value.fieldsLabels.size) - repositionFieldDialog.showRepositionNoteTypeFieldDialog { position -> - launchCatchingTask { - val isConfirmed = userAcceptsSchemaChange() - if (!isConfirmed) return@launchCatchingTask - Timber.i("Repositioning field from %d to %d", viewModel.state.value.currentPos, position) - val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] - viewModel.reposition(field, position) - } - } - } - - /* - * Changes the sort field (that displays in card browser) to the current field - */ - private fun sortByField() { - launchCatchingTask { - val isConfirmed = userAcceptsSchemaChange() - if (!isConfirmed) return@launchCatchingTask - viewModel.changeSort(viewModel.state.value.currentPos) - } - } - - fun handleAction(contextMenuAction: NoteTypeFieldEditorContextMenuAction) { - when (contextMenuAction) { - NoteTypeFieldEditorContextMenuAction.Sort -> sortByField() - NoteTypeFieldEditorContextMenuAction.Reposition -> repositionFieldDialog() - NoteTypeFieldEditorContextMenuAction.Delete -> deleteFieldDialog() - NoteTypeFieldEditorContextMenuAction.Rename -> renameFieldDialog() - NoteTypeFieldEditorContextMenuAction.AddLanguageHint -> localeHintDialog() - } - } - - private fun localeHintDialog() { + private fun localeHintDialog( + locale: Locale?, + position: Int, + ) { Timber.i("displaying locale hint dialog") - // We don't currently show the current value, but we may want to in the future - showDialogFragment(LocaleSelectionDialog()) + showDialogFragment(LocaleSelectionDialog.newInstance(position, locale)) } - /* + /** * Sets the Locale Hint of the field to the provided value. * This allows some keyboard (GBoard) to change language + * @param position the position of the field + * @param selectedLocale the selected locale */ - private fun addFieldLocaleHint(selectedLocale: Locale) { - val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] - viewModel.languageHint(field, selectedLocale) + private fun addFieldLocaleHint( + position: Int, + selectedLocale: Locale?, + ) { + viewModel.languageHint(position, selectedLocale) val format = - getString( - R.string.model_field_editor_language_hint_dialog_success_result, - selectedLocale.displayName, - ) + if (selectedLocale != null) { + getString( + R.string.model_field_editor_language_hint_dialog_success_result, + selectedLocale.displayName, + ) + } else { + getString(R.string.model_field_editor_language_hint_dialog_cleared_result) + } showSnackbar(format, Snackbar.LENGTH_SHORT) - initialize() } @VisibleForTesting(otherwise = VisibleForTesting.NONE) @@ -301,62 +361,139 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field @VisibleForTesting(otherwise = VisibleForTesting.NONE) @Throws(ConfirmModSchemaException::class) - fun renameField(name: String) { + fun renameField( + position: Int, + name: String, + ) { val fieldLabel = uniqueName(name) ?: return - val field = viewModel.state.value.noteFields[viewModel.state.value.currentPos] - viewModel.rename(field, fieldLabel) + viewModel.rename(position, fieldLabel) } - /* - * Returns a list of field names with their kind - * So far the only kind is SORT, which defines the field upon which notes could be sorted - */ - private fun fieldNamesWithKind(): List> = - viewModel.state.value.fieldsLabels.mapIndexed { index, fieldName -> - Pair( - fieldName, - if (index == viewModel.state.value.notetype.sortf) NodetypeKind.SORT else NodetypeKind.UNDEFINED, - ) - } - companion object { const val EXTRA_NOTETYPE_NAME = "extra_notetype_name" const val EXTRA_NOTETYPE_ID = "extra_notetype_id" } } -enum class NodetypeKind { - SORT, - UNDEFINED, -} +private class NoteFieldAdapter( + private val listener: ItemChangeListener, +) : ListAdapter(DIFF_CALLBACK) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ) = NoteFieldViewHolder( + ItemNotetypeFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false), + listener, + ) -internal class NoteFieldAdapter( - private val context: Context, - labels: List>, -) : android.widget.ArrayAdapter>(context, 0, labels) { - override fun getView( + override fun onBindViewHolder( + holder: NoteFieldViewHolder, position: Int, - convertView: View?, - parent: ViewGroup, - ): View { - val binding = - if (convertView != null) { - ItemNotetypeFieldBinding.bind(convertView) - } else { - ItemNotetypeFieldBinding.inflate(LayoutInflater.from(context), parent, false) + ) { + holder.bind(getItem(position)) + } + + override fun onViewRecycled(holder: NoteFieldViewHolder) { + holder.recycled() + } + + companion object { + private val DIFF_CALLBACK = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: NoteTypeFieldRowData, + newItem: NoteTypeFieldRowData, + ) = oldItem.uuid == newItem.uuid + + override fun areContentsTheSame( + oldItem: NoteTypeFieldRowData, + newItem: NoteTypeFieldRowData, + ) = oldItem == newItem } + } - getItem(position)?.let { - val (name, kind) = it - binding.fieldName.text = name - binding.fieldName.setCompoundDrawablesRelativeWithIntrinsicBoundsKt( - end = - when (kind) { - NodetypeKind.SORT -> R.drawable.ic_sort - NodetypeKind.UNDEFINED -> 0 - }, - ) + inner class NoteFieldViewHolder( + private val binding: ItemNotetypeFieldBinding, + listener: ItemChangeListener, + ) : RecyclerView.ViewHolder(binding.root) { + init { + binding.apply { + root.apply { + isFocusable = true + isFocusableInTouchMode = true + } + fieldSortButton.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onSortChanged(position) + } + } + fieldLanguageButton.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onLocaleChangeRequested(position) + } + } + fieldEdit.setOnFocusChangeListener { v, hasFocus -> + if (!hasFocus) { + v.hideKeyboard() + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + val name = (v as TextInputEditText).text?.toString().orEmpty() + val oldName = (bindingAdapter as NoteFieldAdapter).getItem(position).name + if (name.isNotBlank() && name != oldName) { + listener.onNameChanged(position, name) + } else { + v.setText(oldName) + } + } + } + } + fieldEdit.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + root.requestFocus() + } + true + } + fieldEditLayout.setEndIconOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + fieldEdit.setText((bindingAdapter as NoteFieldAdapter).getItem(position).name) + fieldEdit.moveCursorToEnd() + } + fieldEditLayout.isEndIconVisible = false + } + } } - return binding.root + + fun bind(item: NoteTypeFieldRowData) { + binding.apply { + fieldEdit.setText(item.name) + fieldSortButton.isChecked = item.isOrder + fieldLanguageButton.isChecked = item.locale != null + } + } + + fun recycled() { + binding.fieldEdit.apply { + setText("") + clearFocus() + } + binding.fieldEditLayout.clearFocus() + binding.root.translationX = 0f + } + } + + interface ItemChangeListener { + fun onNameChanged( + position: Int, + name: String, + ) + + fun onSortChanged(position: Int) + + fun onLocaleChangeRequested(position: Int) + + fun onDeleted(position: Int) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt index e4004cc01578..049b5bb85528 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt @@ -1,14 +1,5 @@ package com.ichi2.anki.notetype.fieldeditor -import com.ichi2.anki.libanki.Fields -import com.ichi2.anki.libanki.NoteTypeId -import com.ichi2.anki.libanki.NotetypeJson - data class NoteTypeFieldEditorState( - val notetype: NotetypeJson, - val noteTypeId: NoteTypeId = 0, - val currentPos: Int = 0, - val noteFields: Fields = notetype.fields, - val fieldsLabels: List = notetype.fieldsNames, - val isLoading: Boolean = false, + val fields: List, ) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index 9255359c816f..8633476c17a2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -5,9 +5,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ichi2.anki.CollectionManager.getColUnsafe import com.ichi2.anki.CollectionManager.withCol -import com.ichi2.anki.libanki.Field +import com.ichi2.anki.common.utils.ext.indexOfOrNull +import com.ichi2.anki.libanki.NotetypeJson import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor.Companion.EXTRA_NOTETYPE_ID import com.ichi2.anki.servicelayer.LanguageHint +import com.ichi2.anki.servicelayer.LanguageHintService.clearLanguageHintForField import com.ichi2.anki.servicelayer.LanguageHintService.languageHint import com.ichi2.anki.servicelayer.LanguageHintService.setLanguageHintForField import kotlinx.coroutines.flow.MutableStateFlow @@ -16,17 +18,17 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Collections +import java.util.UUID class NoteTypeFieldEditorViewModel( - private val savedStateHandle: SavedStateHandle, + savedStateHandle: SavedStateHandle, ) : ViewModel() { + val noteTypeId = savedStateHandle.get(EXTRA_NOTETYPE_ID) ?: 0 private val _state by lazy { - val noteTypeID = savedStateHandle.get(EXTRA_NOTETYPE_ID) ?: 0 - val notetype = getColUnsafe().notetypes.get(noteTypeID)!! MutableStateFlow( NoteTypeFieldEditorState( - notetype = notetype, - noteTypeId = noteTypeID, + fields = getColUnsafe().notetypes.get(noteTypeId)!!.data, ), ) } @@ -34,112 +36,253 @@ class NoteTypeFieldEditorViewModel( fun initialize() { viewModelScope.launch { - refreshNoteTypes() + forceRefresh() } } - fun updateCurrentPosition(position: Int) { - _state.update { oldState -> oldState.copy(currentPos = position) } - } - + /** + * Create new field with the given name + * @param name the name of the field, which must be unique, not empty, allowed to the notetype + */ fun add(name: String) { Timber.d("doInBackgroundAddField") - _state.update { oldState -> oldState.copy(isLoading = true) } + viewModelScope.launch { runCatching { withCol { - val notetype = notetypes.get(state.value.noteTypeId)!! + val notetype = notetypes.get(noteTypeId)!! notetypes.addField(notetype, notetypes.newField(name)) notetypes.save(notetype) } - } - initialize() + }.fold( + onSuccess = { _state.updateData { this@updateData.add(NoteTypeFieldRowData(name = name)) } }, + onFailure = { forceRefresh() }, + ) } } + /** + * Rename the existing field with the given name + * @param pos the position of the field to rename + * @param name the name of the field, which must be unique, not empty, addowed to the notetype + */ fun rename( - field: Field, + pos: Int, name: String, ) { Timber.d("doInBackgroundRenameField") - _state.update { oldState -> oldState.copy(isLoading = true) } viewModelScope.launch { runCatching { withCol { - val notetype = notetypes.get(state.value.noteTypeId)!! + val notetype = notetypes.get(noteTypeId)!! + val field = notetype.fields[pos] notetypes.renameField(notetype, field, name) notetypes.save(notetype) } - } - initialize() + }.fold( + onSuccess = { _state.updateData { this[pos] = this[pos].copy(name = name) } }, + onFailure = { forceRefresh() }, + ) } } - fun delete(field: Field) { - Timber.d("doInBackGroundDeleteField") - _state.update { oldState -> oldState.copy(isLoading = true) } + /** + * Delete the existing field with the given name + * @param pos the position of the field to delete + */ + fun delete(pos: Int) { + Timber.d("doInBackgroundDeleteField") viewModelScope.launch { runCatching { withCol { - val notetype = notetypes.get(state.value.noteTypeId)!! + val notetype = notetypes.get(noteTypeId)!! + val field = notetype.fields[pos] notetypes.removeField(notetype, field) notetypes.save(notetype) } - } - initialize() + }.fold( + onSuccess = { _state.updateData { removeAt(pos) } }, + onFailure = { forceRefresh() }, + ) } } + /** + * Set the existing field as the order field + * @param position the position of the target field + */ fun changeSort(position: Int) { Timber.d("doInBackgroundChangeSortField") viewModelScope.launch { runCatching { withCol { - val notetype = notetypes.get(state.value.noteTypeId)!! + val notetype = notetypes.get(noteTypeId)!! + val oldSortPosition = notetype.sortf notetypes.setSortIndex(notetype, position) notetypes.save(notetype) + return@withCol oldSortPosition } - } - initialize() + }.fold( + onSuccess = { oldPosition -> + _state.updateData { + this[oldPosition] = this[oldPosition].copy(isOrder = false) + this[position] = this[position].copy(isOrder = true) + } + }, + onFailure = { forceRefresh() }, + ) } } + /** + * Move the existing field to the given position in ViewState + * + * This method does NOT persist changes to the database + * + * @param oldPosition the current position of the target field + * @param newPosition the new position of the target field + * @see NoteTypeFieldEditorViewModel.reposition + */ + fun visuallyReposition( + oldPosition: Int, + newPosition: Int, + ) { + _state.update { oldValue -> + val fields = oldValue.fields.toMutableList() + Collections.swap(fields, oldPosition, newPosition) + oldValue.copy(fields = fields.toList()) + } + } + + /** + * Move the existing field to the given position in ViewState + * + * This method DOES persist changes to the database + * + * @param oldPosition the current position of the target field + * @param newPosition the new position of the target field + * @see NoteTypeFieldEditorViewModel.visuallyReposition + */ fun reposition( - field: Field, - position: Int, + oldPosition: Int, + newPosition: Int, ) { Timber.d("doInBackgroundRepositionField") + Timber.i("Repositioning field from %d to %d", oldPosition, newPosition) viewModelScope.launch { runCatching { withCol { - val notetype = notetypes.get(state.value.noteTypeId)!! - notetypes.repositionField(notetype, field, position) + val notetype = notetypes.get(noteTypeId)!! + val field = notetype.fields[oldPosition] + notetypes.repositionField(notetype, field, newPosition) + notetypes.save(notetype) } - } - initialize() + }.fold( + onSuccess = { smartRefresh() }, + onFailure = { forceRefresh() }, + ) } } + /** + * Set a [Locale] as the keyboard hint for the field + * + * @param position the position of the target field + * @param locale the [Locale] to set, or null to clear the hint + * @see LanguageHintService + */ fun languageHint( - field: Field, - locale: LanguageHint, + position: Int, + locale: LanguageHint?, ) { - field.languageHint viewModelScope.launch { runCatching { withCol { - setLanguageHintForField(notetypes, state.value.notetype, state.value.currentPos, locale) + val notetype = notetypes.get(noteTypeId)!! + if (locale != null) { + setLanguageHintForField(notetypes, notetype, position, locale) + } else { + clearLanguageHintForField(notetypes, notetype, position) + } } - } - initialize() + }.fold( + onSuccess = { _state.updateData { this[position] = this[position].copy(locale = locale) } }, + onFailure = { forceRefresh() }, + ) } } - fun undo() { + /** + * Refresh the data of the target field on the given position + * + * This is used to clear user's change which is not applied to the database + * @param position the position of the target field + * @see NoteTypeFieldEditorViewModel.smartRefresh() + * @see NoteTypeFieldEditorViewModel.forceRefresh() + */ + fun forceRefresh(position: Int) { + viewModelScope.launch { + _state.updateData { this[position] = this[position].copy(uuid = UUID.randomUUID().toString()) } + } } - suspend fun refreshNoteTypes() { - val notetype = withCol { notetypes.get(state.value.noteTypeId)!! } - _state.update { oldState -> oldState.copy(isLoading = false, notetype = notetype) } + /** + * Obtain the data from the database and update the state without the identifying data of the data changing + * + * This tries to keep the identifying data of the data of the fields if possible to avoid innecessary UI refreshes. + * This can be invoked ONLY IF the data is NOT corrupted + * and ONLY AFTER you updated the database successfully. + * @see NoteTypeFieldEditorViewModel.forceRefresh(Int) + * @see NoteTypeFieldEditorViewModel.forceRefresh() + */ + suspend fun smartRefresh() { + val oldData = state.value.fields + val newData = withCol { notetypes.get(noteTypeId)!! }.data + val oldDataNameList = oldData.map { it.name } + val newNamedDataIndex = newData.indexOfOrNull { !oldDataNameList.contains(it.name) } + val isRenamed = oldData.size == newData.size && newNamedDataIndex != null + val updateData = + newData.mapIndexed { index, new -> + if (isRenamed && index == newNamedDataIndex) { + // when renamed, index is not changed. + val renamedDataIndex = newNamedDataIndex + // succeed to the previous uuid referring to index + new.copy(uuid = oldData[renamedDataIndex].uuid) + } else { + // succeed to the previous uuid referring to name + new.copy(uuid = oldData.firstOrNull { it.name == new.name }?.uuid ?: UUID.randomUUID().toString()) + } + } + _state.update { oldState -> oldState.copy(fields = updateData) } + } + + /** + * Obtain the data from the database and update the state + * This completely overrides the state. + * This is useful when something went wrong or you want to update the whole data. + * @see NoteTypeFieldEditorViewModel.forceRefresh(Int) + * @see NoteTypeFieldEditorViewModel.smartRefresh() + */ + suspend fun forceRefresh() { + val data = withCol { notetypes.get(noteTypeId)!! }.data + _state.update { oldState -> oldState.copy(fields = data) } + } + + private fun MutableStateFlow.updateData(edit: MutableList.() -> Unit) = + update { + val fields = it.fields.toMutableList() + edit.invoke(fields) + it.copy(fields = fields.toList()) + } + + private val NotetypeJson.data: List get() { + val sortF = sortf + return fields.mapIndexed { index, it -> + NoteTypeFieldRowData( + name = it.name, + isOrder = index == sortF, + locale = it.languageHint, + ) + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt new file mode 100644 index 000000000000..e13c5a7cc9d4 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt @@ -0,0 +1,11 @@ +package com.ichi2.anki.notetype.fieldeditor + +import java.util.Locale +import java.util.UUID + +data class NoteTypeFieldRowData( + val uuid: String = UUID.randomUUID().toString(), + val name: String, + val isOrder: Boolean = false, + val locale: Locale? = null, +) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RenameNoteTypeField.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RenameNoteTypeField.kt deleted file mode 100644 index 71a32b11977b..000000000000 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/RenameNoteTypeField.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.ichi2.anki.notetype.fieldeditor - -import androidx.appcompat.app.AlertDialog -import com.ichi2.anki.R -import com.ichi2.anki.launchCatchingTask -import com.ichi2.anki.sync.userAcceptsSchemaChange -import com.ichi2.ui.FixedEditText -import com.ichi2.utils.customView -import com.ichi2.utils.moveCursorToEnd -import com.ichi2.utils.negativeButton -import com.ichi2.utils.positiveButton -import com.ichi2.utils.show -import com.ichi2.utils.title - -class RenameNoteTypeField( - private val activity: NoteTypeFieldEditor, - val fieldName: String, -) { - fun showRenameNoteTypeFieldDialog(confirm: (name: String) -> Unit) { - val fieldNameInput = - FixedEditText(activity).apply { - focusWithKeyboard() - isSingleLine = true - setText(fieldName) - moveCursorToEnd() - } - - activity.apply { - launchCatchingTask { - val confirmation = userAcceptsSchemaChange() - if (!confirmation) { - return@launchCatchingTask - } - AlertDialog - .Builder(activity) - .show { - customView(fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) - title(R.string.model_field_editor_rename) - positiveButton(R.string.rename) { - confirm(fieldNameInput.text.toString()) - } - negativeButton(R.string.dialog_cancel) - } - } - } - } -} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/LanguageHintService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/LanguageHintService.kt index 275d07d98a22..b55b72cf1046 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/LanguageHintService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/LanguageHintService.kt @@ -48,6 +48,23 @@ object LanguageHintService { Timber.i("Set field locale to %s", selectedLocale) } + fun clearLanguageHintForField( + notetypes: Notetypes, + notetype: NotetypeJson, + fieldPos: Int, + ) { + val field = notetype.getField(fieldPos) + field.languageHint = null + notetypes.save(notetype) + + Timber.i("Clear field locale") + } + + fun compareLanguage( + locale1: Locale, + locale2: Locale, + ) = locale1.toLanguageTag() == locale2.toLanguageTag() + fun EditText.applyLanguageHint(languageHint: LanguageHint?) { this.imeHintLocales = if (languageHint != null) LocaleList(languageHint) else null } diff --git a/AnkiDroid/src/main/res/color/edit_field_icon_button_tint.xml b/AnkiDroid/src/main/res/color/edit_field_icon_button_tint.xml new file mode 100644 index 000000000000..6872b0a20855 --- /dev/null +++ b/AnkiDroid/src/main/res/color/edit_field_icon_button_tint.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/drawable/field_check.xml b/AnkiDroid/src/main/res/drawable/field_check.xml new file mode 100644 index 000000000000..4c799f4a9192 --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/field_check.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/AnkiDroid/src/main/res/drawable/ic_edit_keyboard.xml b/AnkiDroid/src/main/res/drawable/ic_edit_keyboard.xml new file mode 100644 index 000000000000..793828592307 --- /dev/null +++ b/AnkiDroid/src/main/res/drawable/ic_edit_keyboard.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/AnkiDroid/src/main/res/layout/item_locale_dialog_fragment.xml b/AnkiDroid/src/main/res/layout/item_locale_dialog_fragment.xml new file mode 100644 index 000000000000..1ebfef472bee --- /dev/null +++ b/AnkiDroid/src/main/res/layout/item_locale_dialog_fragment.xml @@ -0,0 +1,53 @@ + + + + + + + + diff --git a/AnkiDroid/src/main/res/layout/item_notetype_field.xml b/AnkiDroid/src/main/res/layout/item_notetype_field.xml index 612150f646e4..5eb921c625bc 100644 --- a/AnkiDroid/src/main/res/layout/item_notetype_field.xml +++ b/AnkiDroid/src/main/res/layout/item_notetype_field.xml @@ -8,29 +8,68 @@ android:minHeight="?android:attr/listPreferredItemHeight" android:background="?attr/selectableItemBackground"> - + app:srcCompat="@drawable/ic_menu_24" + /> - + + + + + + diff --git a/AnkiDroid/src/main/res/layout/locale_dialog_fragment_textview.xml b/AnkiDroid/src/main/res/layout/locale_dialog_fragment_textview.xml deleted file mode 100644 index 2aec88078592..000000000000 --- a/AnkiDroid/src/main/res/layout/locale_dialog_fragment_textview.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/AnkiDroid/src/main/res/layout/locale_selection_dialog.xml b/AnkiDroid/src/main/res/layout/locale_selection_dialog.xml index ef353426cf38..e098c04fc892 100644 --- a/AnkiDroid/src/main/res/layout/locale_selection_dialog.xml +++ b/AnkiDroid/src/main/res/layout/locale_selection_dialog.xml @@ -33,7 +33,7 @@ app:title="@string/locale_selection_dialog_title_new" app:popupTheme="@style/ActionBar.Popup" app:navigationContentDescription="@string/abc_action_bar_up_description" - app:navigationIcon="@drawable/close_icon" + app:navigationIcon="@drawable/ic_delete" /> - - - diff --git a/AnkiDroid/src/main/res/values/17-model-manager.xml b/AnkiDroid/src/main/res/values/17-model-manager.xml index 7564e3f2b8a4..60e6e03c6fee 100644 --- a/AnkiDroid/src/main/res/values/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values/17-model-manager.xml @@ -38,6 +38,7 @@ Set language hint to %s + Cleared language hint Add field diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt index 074d565bcc21..45651cecedd5 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt @@ -105,6 +105,7 @@ class NoteTypeFieldEditorTest( positiveButton(text = "") { try { val noteTypeName = "Basic" + val position = 0 val fieldName = fieldNameInput.text.toString() // start ModelFieldEditor activity @@ -125,7 +126,7 @@ class NoteTypeFieldEditorTest( ) when (fieldOperationType) { FieldOperationType.ADD_FIELD -> noteTypeFieldEditor.addField(fieldName) - FieldOperationType.RENAME_FIELD -> noteTypeFieldEditor.renameField(fieldName) + FieldOperationType.RENAME_FIELD -> noteTypeFieldEditor.renameField(position, fieldName) } } catch (exception: ConfirmModSchemaException) { throw RuntimeException(exception) From b60e262c1e5ad474a2ec7e2aed76e72c6d116554 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:13:00 +0900 Subject: [PATCH 05/19] fix(notetypefieldeditor): update recyclerview even when delete is canceled. Reset view.transitionX by calling notifyItemChanged method. --- .../fieldeditor/NoteTypeFieldEditor.kt | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 6ed8d5d361b9..19bf2467d280 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -64,7 +64,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field private val binding by viewBinding(NoteTypeFieldEditorBinding::bind) val viewModel by viewModels() - private val adapter by lazy { + private val adapter: NoteFieldAdapter by lazy { val listener = object : NoteFieldAdapter.ItemChangeListener { override fun onNameChanged( @@ -79,7 +79,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field viewModel.rename(position, validName) } else { // clear temporary edittext changes - viewModel.forceRefresh(position) + adapter.notifyItemChanged(position) } } } @@ -98,10 +98,6 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field .locale localeHintDialog(locale, position) } - - override fun onDeleted(position: Int) { - deleteFieldDialog(position) - } } return@lazy NoteFieldAdapter(listener) } @@ -130,6 +126,8 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field val position = viewHolder.bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { deleteFieldDialog(position) + // reset transitionX whether the field is deleted or not + viewHolder.bindingAdapter?.notifyItemChanged(position) } } @@ -297,10 +295,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field val isConfirmed = userAcceptsSchemaChange() if (isConfirmed) { viewModel.delete(position) - } else { - viewModel.forceRefresh(position) } - // This ensures that the context menu closes after the field has been deleted supportFragmentManager.popBackStackImmediate( null, @@ -308,10 +303,6 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) } } - it.setCancel { - viewModel.forceRefresh(position) - } - it.isCancelable = false showDialogFragment(it) } } @@ -493,7 +484,5 @@ private class NoteFieldAdapter( fun onSortChanged(position: Int) fun onLocaleChangeRequested(position: Int) - - fun onDeleted(position: Int) } } From 473c8d723b2f31dd442be5105703a5bb38f5339e Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Mon, 9 Mar 2026 20:18:38 +0900 Subject: [PATCH 06/19] fix(notetypefieldeditor):undo notetype change (except for Languagehint) Record changes to make it possible to undo from deck picker screen. Extra: undo delete multiple Notetypes operation at a once. --- .../anki/notetype/ManageNoteTypesViewModel.kt | 4 + .../NoteTypeFieldEditorViewModel.kt | 344 ++++++++++++------ 2 files changed, 230 insertions(+), 118 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt index 8a354968cc11..5d6745458814 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt @@ -247,6 +247,7 @@ class ManageNoteTypesViewModel : ViewModel() { } viewModelScope.launch { val errors = mutableMapOf() + val lastStep = withCol { addCustomUndoEntry(TR.actionsUpdateNotetype()) } noteTypesToDelete.forEach { noteType -> undoableOp { safeRemoveNoteType(noteType.id) @@ -257,6 +258,9 @@ class ManageNoteTypesViewModel : ViewModel() { OpChanges.getDefaultInstance() } } + undoableOp { + mergeUndoEntries(lastStep) + } // look through any errors we might have and remove from our list of note types the ones // that were in noteTypesToDelete but not in errors map(which presumably weren't deleted) val removedIds = noteTypesToDelete.map { it.id } - errors.keys.map { it.id }.toSet() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index 8633476c17a2..8c133422b43a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -3,15 +3,22 @@ package com.ichi2.anki.notetype.fieldeditor import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ichi2.anki.CollectionManager.getColUnsafe +import anki.collection.OpChanges +import anki.collection.copy +import anki.notetypes.Notetype +import anki.notetypes.copy import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.common.utils.ext.indexOfOrNull -import com.ichi2.anki.libanki.NotetypeJson +import com.ichi2.anki.libanki.Collection +import com.ichi2.anki.libanki.NoteTypeId +import com.ichi2.anki.libanki.UndoStepCounter +import com.ichi2.anki.libanki.backend.BackendUtils.toJsonBytes +import com.ichi2.anki.libanki.getNotetype +import com.ichi2.anki.libanki.updateNotetype import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor.Companion.EXTRA_NOTETYPE_ID +import com.ichi2.anki.observability.undoableOp import com.ichi2.anki.servicelayer.LanguageHint -import com.ichi2.anki.servicelayer.LanguageHintService.clearLanguageHintForField import com.ichi2.anki.servicelayer.LanguageHintService.languageHint -import com.ichi2.anki.servicelayer.LanguageHintService.setLanguageHintForField import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -25,16 +32,17 @@ class NoteTypeFieldEditorViewModel( savedStateHandle: SavedStateHandle, ) : ViewModel() { val noteTypeId = savedStateHandle.get(EXTRA_NOTETYPE_ID) ?: 0 + var undoStepCounter: UndoStepCounter? = null private val _state by lazy { MutableStateFlow( NoteTypeFieldEditorState( - fields = getColUnsafe().notetypes.get(noteTypeId)!!.data, + fields = emptyList(), ), ) } val state: StateFlow = _state.asStateFlow() - fun initialize() { + init { viewModelScope.launch { forceRefresh() } @@ -46,64 +54,49 @@ class NoteTypeFieldEditorViewModel( */ fun add(name: String) { Timber.d("doInBackgroundAddField") - viewModelScope.launch { - runCatching { - withCol { - val notetype = notetypes.get(noteTypeId)!! - notetypes.addField(notetype, notetypes.newField(name)) - notetypes.save(notetype) - } - }.fold( - onSuccess = { _state.updateData { this@updateData.add(NoteTypeFieldRowData(name = name)) } }, - onFailure = { forceRefresh() }, - ) + mergeUndoableOp { + safeAddField(noteTypeId, name) + .onSuccess { smartRefresh() } + .onFailure { forceRefresh() } + .getOrDefault(OpChanges.getDefaultInstance()) + } } } /** * Rename the existing field with the given name - * @param pos the position of the field to rename + * @param position the position of the field to rename * @param name the name of the field, which must be unique, not empty, addowed to the notetype */ fun rename( - pos: Int, + position: Int, name: String, ) { Timber.d("doInBackgroundRenameField") viewModelScope.launch { - runCatching { - withCol { - val notetype = notetypes.get(noteTypeId)!! - val field = notetype.fields[pos] - notetypes.renameField(notetype, field, name) - notetypes.save(notetype) - } - }.fold( - onSuccess = { _state.updateData { this[pos] = this[pos].copy(name = name) } }, - onFailure = { forceRefresh() }, - ) + mergeUndoableOp { + safeRenameField(noteTypeId, position, name) + .onSuccess { smartRefresh() } + .onFailure { forceRefresh() } + .getOrDefault(OpChanges.getDefaultInstance()) + } } } /** * Delete the existing field with the given name - * @param pos the position of the field to delete + * @param position the position of the field to delete */ - fun delete(pos: Int) { + fun delete(position: Int) { Timber.d("doInBackgroundDeleteField") viewModelScope.launch { - runCatching { - withCol { - val notetype = notetypes.get(noteTypeId)!! - val field = notetype.fields[pos] - notetypes.removeField(notetype, field) - notetypes.save(notetype) - } - }.fold( - onSuccess = { _state.updateData { removeAt(pos) } }, - onFailure = { forceRefresh() }, - ) + mergeUndoableOp { + safeDeleteField(noteTypeId, position) + .onSuccess { smartRefresh() } + .onFailure { forceRefresh() } + .getOrDefault(OpChanges.getDefaultInstance()) + } } } @@ -114,23 +107,12 @@ class NoteTypeFieldEditorViewModel( fun changeSort(position: Int) { Timber.d("doInBackgroundChangeSortField") viewModelScope.launch { - runCatching { - withCol { - val notetype = notetypes.get(noteTypeId)!! - val oldSortPosition = notetype.sortf - notetypes.setSortIndex(notetype, position) - notetypes.save(notetype) - return@withCol oldSortPosition - } - }.fold( - onSuccess = { oldPosition -> - _state.updateData { - this[oldPosition] = this[oldPosition].copy(isOrder = false) - this[position] = this[position].copy(isOrder = true) - } - }, - onFailure = { forceRefresh() }, - ) + mergeUndoableOp { + safeChangeSort(noteTypeId, position) + .onSuccess { smartRefresh() } + .onFailure { forceRefresh() } + .getOrDefault(OpChanges.getDefaultInstance()) + } } } @@ -170,17 +152,12 @@ class NoteTypeFieldEditorViewModel( Timber.d("doInBackgroundRepositionField") Timber.i("Repositioning field from %d to %d", oldPosition, newPosition) viewModelScope.launch { - runCatching { - withCol { - val notetype = notetypes.get(noteTypeId)!! - val field = notetype.fields[oldPosition] - notetypes.repositionField(notetype, field, newPosition) - notetypes.save(notetype) - } - }.fold( - onSuccess = { smartRefresh() }, - onFailure = { forceRefresh() }, - ) + mergeUndoableOp { + safeReposition(noteTypeId, oldPosition, newPosition) + .onSuccess { smartRefresh() } + .onFailure { forceRefresh() } + .getOrDefault(OpChanges.getDefaultInstance()) + } } } @@ -196,19 +173,15 @@ class NoteTypeFieldEditorViewModel( locale: LanguageHint?, ) { viewModelScope.launch { - runCatching { - withCol { - val notetype = notetypes.get(noteTypeId)!! - if (locale != null) { - setLanguageHintForField(notetypes, notetype, position, locale) - } else { - clearLanguageHintForField(notetypes, notetype, position) + mergeUndoableOp { + safeChangeLanguageHint(noteTypeId, position, locale) + .onSuccess { smartRefresh() } + .onFailure { forceRefresh() } + .getOrDefault(OpChanges.getDefaultInstance()) + .copy { + notetype = true } - } - }.fold( - onSuccess = { _state.updateData { this[position] = this[position].copy(locale = locale) } }, - onFailure = { forceRefresh() }, - ) + } } } @@ -222,7 +195,13 @@ class NoteTypeFieldEditorViewModel( */ fun forceRefresh(position: Int) { viewModelScope.launch { - _state.updateData { this[position] = this[position].copy(uuid = UUID.randomUUID().toString()) } + _state.update { + val list = it.fields.toMutableList() + list.apply { + this[position] = this[position].copy(uuid = UUID.randomUUID().toString()) + } + it.copy(fields = list.toList()) + } } } @@ -235,25 +214,27 @@ class NoteTypeFieldEditorViewModel( * @see NoteTypeFieldEditorViewModel.forceRefresh(Int) * @see NoteTypeFieldEditorViewModel.forceRefresh() */ - suspend fun smartRefresh() { - val oldData = state.value.fields - val newData = withCol { notetypes.get(noteTypeId)!! }.data - val oldDataNameList = oldData.map { it.name } - val newNamedDataIndex = newData.indexOfOrNull { !oldDataNameList.contains(it.name) } - val isRenamed = oldData.size == newData.size && newNamedDataIndex != null - val updateData = - newData.mapIndexed { index, new -> - if (isRenamed && index == newNamedDataIndex) { - // when renamed, index is not changed. - val renamedDataIndex = newNamedDataIndex - // succeed to the previous uuid referring to index - new.copy(uuid = oldData[renamedDataIndex].uuid) - } else { - // succeed to the previous uuid referring to name - new.copy(uuid = oldData.firstOrNull { it.name == new.name }?.uuid ?: UUID.randomUUID().toString()) + fun smartRefresh() { + viewModelScope.launch { + val oldData = state.value.fields + val newData = withCol { obtainData() } + val oldDataNameList = oldData.map { it.name } + val newNamedDataIndex = newData.indexOfOrNull { !oldDataNameList.contains(it.name) } + val isRenamed = oldData.size == newData.size && newNamedDataIndex != null + val updateData = + newData.mapIndexed { index, new -> + if (isRenamed && index == newNamedDataIndex) { + // when renamed, index is not changed. + val renamedDataIndex = newNamedDataIndex + // succeed to the previous uuid referring to index + new.copy(uuid = oldData[renamedDataIndex].uuid) + } else { + // succeed to the previous uuid referring to name + new.copy(uuid = oldData.firstOrNull { it.name == new.name }?.uuid ?: UUID.randomUUID().toString()) + } } - } - _state.update { oldState -> oldState.copy(fields = updateData) } + _state.update { oldState -> oldState.copy(fields = updateData) } + } } /** @@ -263,26 +244,153 @@ class NoteTypeFieldEditorViewModel( * @see NoteTypeFieldEditorViewModel.forceRefresh(Int) * @see NoteTypeFieldEditorViewModel.smartRefresh() */ - suspend fun forceRefresh() { - val data = withCol { notetypes.get(noteTypeId)!! }.data - _state.update { oldState -> oldState.copy(fields = data) } + fun forceRefresh() { + viewModelScope.launch { + _state.update { oldValue -> + val data = withCol { obtainData() } + oldValue.copy(fields = data) + } + } } - private fun MutableStateFlow.updateData(edit: MutableList.() -> Unit) = - update { - val fields = it.fields.toMutableList() - edit.invoke(fields) - it.copy(fields = fields.toList()) + private suspend fun mergeUndoableOp(block: Collection.() -> T) { + if (undoStepCounter == null) { + withCol { + undoStepCounter = addCustomUndoEntry(tr.actionsUpdateNotetype()) + } + } + undoableOp { + block() } + val step = undoStepCounter ?: return + undoableOp { + mergeUndoEntries(step) + } + } - private val NotetypeJson.data: List get() { - val sortF = sortf - return fields.mapIndexed { index, it -> - NoteTypeFieldRowData( - name = it.name, - isOrder = index == sortF, - locale = it.languageHint, - ) + private fun Collection.obtainData(): List { + val langugageMap = + notetypes.get(noteTypeId)!!.fields.associate { + it.name to it.languageHint + } + return getNotetype(noteTypeId).run { + val sortF = config.sortFieldIdx + fieldsList.mapIndexed { index, it -> + NoteTypeFieldRowData( + name = it.name, + isOrder = index == sortF, + locale = langugageMap.getOrDefault(it.name, null), + ) + } } } + + private fun Collection.safeAddField( + ntid: NoteTypeId, + newName: String, + ) = runCatching { + val notetype = + getNotetype(ntid).copy { + fields.apply { + val field = + Notetype.Field + .newBuilder() + .setName(newName) + .build() + add(field) + } + } + updateNotetype(notetype) + } + + private fun Collection.safeRenameField( + ntid: NoteTypeId, + position: Int, + newName: String, + ) = runCatching { + val notetype = + getNotetype(ntid).copy { + fields.apply { + val field = this[position] + this[position] = field.copy { name = newName } + } + } + updateNotetype(notetype) + } + + private fun Collection.safeDeleteField( + ntid: NoteTypeId, + position: Int, + ) = runCatching { + val notetype = + getNotetype(ntid).copy { + fields.apply { + val list = this.toMutableList().apply { removeAt(position) }.toList() + clear() + addAll(list) + } + } + updateNotetype(notetype) + } + + private fun Collection.safeReposition( + ntid: NoteTypeId, + oldPosition: Int, + newPosition: Int, + ) = runCatching { + val notetype = + getNotetype(ntid).copy { + fields.apply { + val list = + this + .toMutableList() + .apply { + val field = this.removeAt(oldPosition) + this.add(newPosition, field) + }.toList() + clear() + addAll(list) + } + } + updateNotetype(notetype) + } + + private fun Collection.safeChangeSort( + ntid: NoteTypeId, + position: Int, + ) = runCatching { + val notetype = + getNotetype(ntid).copy { + val newConfig = configOrNull ?: Notetype.Config.newBuilder().build() + config = newConfig.copy { sortFieldIdx = position } + } + updateNotetype(notetype) + } + + private fun Collection.safeChangeLanguageHint( + ntid: NoteTypeId, + position: Int, + selectedLocale: LanguageHint?, + ) = runCatching { + // notetypes.save(notetype) (which call addOrUpdateNotetype()) will delete OpChanges, so we need to convert Field to Notetype.Field + val notetypeJson = notetypes.get(ntid)!! + val field = notetypeJson.getField(position) + field.languageHint = selectedLocale + + val notetype = + getNotetype(ntid).copy { + fields.apply { + val list = + this + .toMutableList() + .apply { + this[position] = Notetype.Field.parseFrom(toJsonBytes(field)) + }.toList() + clear() + addAll(list) + } + } + Timber.i("Set field locale to %s", selectedLocale) + updateNotetype(notetype) + } } From edc921fc5d98fec4e84ee6bec1ba6415d9e9dbba Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:30:06 +0900 Subject: [PATCH 07/19] feat(notetypefieldeditor): Add a note for the limitation of undo feature in notetypefield screen. --- .../fieldeditor/NoteTypeFieldEditor.kt | 26 ++++++++++++++++++- .../main/res/menu/notetype_field_editor.xml | 23 ++++++++++++++++ .../src/main/res/values/17-model-manager.xml | 2 ++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 AnkiDroid/src/main/res/menu/notetype_field_editor.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 19bf2467d280..2be8e4cb92a4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -18,10 +18,12 @@ package com.ichi2.anki.notetype.fieldeditor import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu import android.view.ViewGroup import android.view.inputmethod.EditorInfo import androidx.activity.viewModels import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog import androidx.core.os.BundleCompat import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle @@ -53,7 +55,11 @@ import com.ichi2.anki.utils.ext.getIntOrNull import com.ichi2.anki.utils.ext.setFragmentResultListener import com.ichi2.anki.utils.ext.showDialogFragment import com.ichi2.anki.utils.hideKeyboard +import com.ichi2.utils.message import com.ichi2.utils.moveCursorToEnd +import com.ichi2.utils.positiveButton +import com.ichi2.utils.show +import com.ichi2.utils.title import dev.androidbroadcast.vbpd.viewBinding import kotlinx.coroutines.launch import timber.log.Timber @@ -210,8 +216,26 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field } } } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.notetype_field_editor, menu) + + menu.findItem(R.id.notetype_field_note).setOnMenuItemClickListener { + AlertDialog + .Builder(this) + .show { + setIcon(R.drawable.ic_dialog_info) + title(R.string.model_field_editor_note_title) + message(R.string.model_field_editor_note_description) + positiveButton(R.string.dialog_ok) + } + true + } + return true + } + // ---------------------------------------------------------------------------- - // CONTEXT MENU DIALOGUES + // ACTION DIALOGUES // ---------------------------------------------------------------------------- /** diff --git a/AnkiDroid/src/main/res/menu/notetype_field_editor.xml b/AnkiDroid/src/main/res/menu/notetype_field_editor.xml new file mode 100644 index 000000000000..9edca1a47c15 --- /dev/null +++ b/AnkiDroid/src/main/res/menu/notetype_field_editor.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/values/17-model-manager.xml b/AnkiDroid/src/main/res/values/17-model-manager.xml index 60e6e03c6fee..8bb09f8b83c1 100644 --- a/AnkiDroid/src/main/res/values/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values/17-model-manager.xml @@ -48,6 +48,8 @@ Reposition field Updating fields Sort by this field + Note + Changes on this screen can be undone via the deck-picker menu. Note that language settings cannot be undone due to limitations in Anki and AnkiDroid\'s backend integration. From a93d27de8523fa7a7ccd02d7cc4fc73d7263e279 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:36:02 +0900 Subject: [PATCH 08/19] refactor(notetypefieldeditor): remove unnecessary user confirmation --- .../notetype/fieldeditor/NoteTypeFieldEditor.kt | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 2be8e4cb92a4..97031012259b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -79,9 +79,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) { launchCatchingTask { val validName = uniqueName(name) - val isValidName = validName != null - val isConfirmed = isValidName && userAcceptsSchemaChange() - if (isConfirmed) { + if (validName != null) { viewModel.rename(position, validName) } else { // clear temporary edittext changes @@ -159,13 +157,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field dragFromPosition != dragToPosition ) { launchCatchingTask { - val isConfirmed = userAcceptsSchemaChange() - if (isConfirmed) { - viewModel.reposition(dragFromPosition, dragToPosition) - } else { - // clear list order changes - viewModel.smartRefresh() - } + viewModel.reposition(dragFromPosition, dragToPosition) } } @@ -316,10 +308,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) it.setConfirm { launchCatchingTask { - val isConfirmed = userAcceptsSchemaChange() - if (isConfirmed) { - viewModel.delete(position) - } + viewModel.delete(position) // This ensures that the context menu closes after the field has been deleted supportFragmentManager.popBackStackImmediate( null, From d840b7d0a5fe5952b72676e6943f33f47595bcbb Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:07:17 +0900 Subject: [PATCH 09/19] feat(notetypefieldeditor): add save button and undo snackbar Add save button to save all changes at once. Show undo snackbar when notetypefields are changed. Show one-way sync confirmation dialog before saving all changes. --- .../anki/notetype/ManageNoteTypesViewModel.kt | 1 + .../fieldeditor/NoteTypeFieldEditor.kt | 300 +++--- .../fieldeditor/NoteTypeFieldEditorState.kt | 34 +- .../NoteTypeFieldEditorViewModel.kt | 889 +++++++++++++----- .../fieldeditor/NoteTypeFieldOperation.kt | 81 ++ .../fieldeditor/NoteTypeFieldRowData.kt | 5 +- .../anki/servicelayer/LanguageHintService.kt | 14 +- .../src/main/res/drawable/field_check.xml | 11 - .../main/res/drawable/ic_edit_keyboard.xml | 3 +- .../layout/item_locale_dialog_fragment.xml | 2 +- .../main/res/layout/item_notetype_field.xml | 5 +- .../main/res/menu/notetype_field_editor.xml | 11 +- .../main/res/values-af/17-model-manager.xml | 4 +- .../main/res/values-am/17-model-manager.xml | 4 +- .../main/res/values-ar/17-model-manager.xml | 4 +- .../main/res/values-az/17-model-manager.xml | 4 +- .../main/res/values-be/17-model-manager.xml | 4 +- .../main/res/values-bg/17-model-manager.xml | 4 +- .../main/res/values-bn/17-model-manager.xml | 4 +- .../main/res/values-ca/17-model-manager.xml | 4 +- .../main/res/values-ckb/17-model-manager.xml | 4 +- .../main/res/values-cs/17-model-manager.xml | 4 +- .../main/res/values-da/17-model-manager.xml | 4 +- .../main/res/values-de/17-model-manager.xml | 4 +- .../main/res/values-el/17-model-manager.xml | 4 +- .../main/res/values-eo/17-model-manager.xml | 4 +- .../res/values-es-rAR/17-model-manager.xml | 4 +- .../res/values-es-rES/17-model-manager.xml | 4 +- .../main/res/values-et/17-model-manager.xml | 4 +- .../main/res/values-eu/17-model-manager.xml | 4 +- .../main/res/values-fa/17-model-manager.xml | 4 +- .../main/res/values-fi/17-model-manager.xml | 4 +- .../main/res/values-fil/17-model-manager.xml | 4 +- .../main/res/values-fr/17-model-manager.xml | 4 +- .../main/res/values-fy/17-model-manager.xml | 4 +- .../main/res/values-ga/17-model-manager.xml | 4 +- .../main/res/values-gl/17-model-manager.xml | 4 +- .../main/res/values-got/17-model-manager.xml | 4 +- .../main/res/values-gu/17-model-manager.xml | 4 +- .../main/res/values-heb/17-model-manager.xml | 4 +- .../main/res/values-hi/17-model-manager.xml | 4 +- .../main/res/values-hr/17-model-manager.xml | 4 +- .../main/res/values-hu/17-model-manager.xml | 4 +- .../main/res/values-hy/17-model-manager.xml | 4 +- .../main/res/values-ind/17-model-manager.xml | 4 +- .../main/res/values-it/17-model-manager.xml | 4 +- .../main/res/values-iw/17-model-manager.xml | 4 +- .../main/res/values-ja/17-model-manager.xml | 4 +- .../main/res/values-ka/17-model-manager.xml | 4 +- .../main/res/values-kk/17-model-manager.xml | 4 +- .../main/res/values-km/17-model-manager.xml | 4 +- .../main/res/values-kn/17-model-manager.xml | 4 +- .../main/res/values-ko/17-model-manager.xml | 4 +- .../main/res/values-ku/17-model-manager.xml | 4 +- .../main/res/values-ky/17-model-manager.xml | 4 +- .../main/res/values-lt/17-model-manager.xml | 4 +- .../main/res/values-lv/17-model-manager.xml | 4 +- .../main/res/values-mk/17-model-manager.xml | 4 +- .../main/res/values-ml/17-model-manager.xml | 4 +- .../main/res/values-mn/17-model-manager.xml | 4 +- .../main/res/values-mr/17-model-manager.xml | 4 +- .../main/res/values-ms/17-model-manager.xml | 4 +- .../main/res/values-my/17-model-manager.xml | 4 +- .../main/res/values-nl/17-model-manager.xml | 4 +- .../main/res/values-nn/17-model-manager.xml | 4 +- .../main/res/values-no/17-model-manager.xml | 4 +- .../main/res/values-or/17-model-manager.xml | 4 +- .../main/res/values-pa/17-model-manager.xml | 4 +- .../main/res/values-pl/17-model-manager.xml | 4 +- .../res/values-pt-rBR/17-model-manager.xml | 4 +- .../res/values-pt-rPT/17-model-manager.xml | 4 +- .../main/res/values-ro/17-model-manager.xml | 4 +- .../main/res/values-ru/17-model-manager.xml | 4 +- .../main/res/values-sat/17-model-manager.xml | 4 +- .../main/res/values-sc/17-model-manager.xml | 4 +- .../main/res/values-sk/17-model-manager.xml | 4 +- .../main/res/values-sl/17-model-manager.xml | 4 +- .../main/res/values-sq/17-model-manager.xml | 4 +- .../main/res/values-sr/17-model-manager.xml | 4 +- .../main/res/values-sv/17-model-manager.xml | 4 +- .../main/res/values-ta/17-model-manager.xml | 4 +- .../main/res/values-te/17-model-manager.xml | 4 +- .../main/res/values-tgl/17-model-manager.xml | 4 +- .../main/res/values-th/17-model-manager.xml | 4 +- .../main/res/values-ti/17-model-manager.xml | 4 +- .../main/res/values-tr/17-model-manager.xml | 4 +- .../main/res/values-tt/17-model-manager.xml | 4 +- .../main/res/values-ug/17-model-manager.xml | 4 +- .../main/res/values-uk/17-model-manager.xml | 4 +- .../main/res/values-ur/17-model-manager.xml | 4 +- .../main/res/values-uz/17-model-manager.xml | 4 +- .../main/res/values-vi/17-model-manager.xml | 4 +- .../res/values-zh-rCN/17-model-manager.xml | 4 +- .../res/values-zh-rTW/17-model-manager.xml | 4 +- .../src/main/res/values/17-model-manager.xml | 18 +- .../com/ichi2/anki/NoteTypeFieldEditorTest.kt | 9 +- 96 files changed, 1137 insertions(+), 574 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt delete mode 100644 AnkiDroid/src/main/res/drawable/field_check.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt index 5d6745458814..10ffafb77994 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.viewModelScope import anki.collection.OpChanges import anki.notetypes.Notetype import anki.notetypes.copy +import com.ichi2.anki.CollectionManager.TR import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.exception.CombinedException import com.ichi2.anki.libanki.Collection diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 97031012259b..ae4ce1f552e8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -17,13 +17,14 @@ package com.ichi2.anki.notetype.fieldeditor import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher import android.view.LayoutInflater import android.view.Menu import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import androidx.activity.addCallback import androidx.activity.viewModels -import androidx.annotation.VisibleForTesting -import androidx.appcompat.app.AlertDialog import androidx.core.os.BundleCompat import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle @@ -34,19 +35,19 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText +import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest import com.ichi2.anki.databinding.ItemNotetypeFieldBinding import com.ichi2.anki.databinding.NoteTypeFieldEditorBinding import com.ichi2.anki.dialogs.ConfirmationDialog +import com.ichi2.anki.dialogs.DiscardChangesDialog import com.ichi2.anki.dialogs.LocaleSelectionDialog import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.KEY_SELECTED_FIELD_POSITION import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.KEY_SELECTED_LOCALE import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.REQUEST_HINT_LOCALE_SELECTION import com.ichi2.anki.launchCatchingTask -import com.ichi2.anki.libanki.exception.ConfirmModSchemaException import com.ichi2.anki.showThemedToast import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.sync.userAcceptsSchemaChange @@ -55,11 +56,7 @@ import com.ichi2.anki.utils.ext.getIntOrNull import com.ichi2.anki.utils.ext.setFragmentResultListener import com.ichi2.anki.utils.ext.showDialogFragment import com.ichi2.anki.utils.hideKeyboard -import com.ichi2.utils.message import com.ichi2.utils.moveCursorToEnd -import com.ichi2.utils.positiveButton -import com.ichi2.utils.show -import com.ichi2.utils.title import dev.androidbroadcast.vbpd.viewBinding import kotlinx.coroutines.launch import timber.log.Timber @@ -78,20 +75,12 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field name: String, ) { launchCatchingTask { - val validName = uniqueName(name) - if (validName != null) { - viewModel.rename(position, validName) - } else { - // clear temporary edittext changes - adapter.notifyItemChanged(position) - } + viewModel.rename(position, name) } } override fun onSortChanged(position: Int) { launchCatchingTask { - val isConfirmed = userAcceptsSchemaChange() - if (!isConfirmed) return@launchCatchingTask viewModel.changeSort(position) } } @@ -185,7 +174,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) val fieldPosition = bundle.getIntOrNull(KEY_SELECTED_FIELD_POSITION) if (fieldPosition != null) { - addFieldLocaleHint(fieldPosition, selectedLocale) + viewModel.setLanguageHint(fieldPosition, selectedLocale) } dismissAllDialogFragments() } @@ -200,10 +189,53 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field touchHelper.attachToRecyclerView(this@apply) } binding.btnAdd.setOnClickListener { addFieldDialog() } + onBackPressedDispatcher.addCallback(this) { + val unsavedChange = adapter.unsavedChange + if (unsavedChange != null) { + viewModel.updateByUuid(unsavedChange) + } + viewModel.requestDiscardChangesAndClose() + } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.state.collect { - adapter.submitList(it.fields) + viewModel.state.collect { state -> + adapter.submitList(state.fields) + when (state.action) { + is NoteTypeFieldEditorState.Action.Undoable -> { + val message = getString(state.action.resId, *state.action.formatArgs.toTypedArray()) + + showUndoSnackbar(message) + } + is NoteTypeFieldEditorState.Action.Error -> + CrashReportService.sendExceptionReport( + state.action.e.source, + NoteTypeFieldEditor::class.java.simpleName, + ) + is NoteTypeFieldEditorState.Action.Rejected -> + showThemedToast( + this@NoteTypeFieldEditor, + getString(state.action.resId), + true, + ) + is NoteTypeFieldEditorState.Action.SaveRequested -> + showSaveChangesDialog(state.action.isNotUndoable, state.action.isSchemaChanges) + NoteTypeFieldEditorState.Action.DiscardRequested -> + showDiscardChangesDialog() + is NoteTypeFieldEditorState.Action.Close -> { + if (state.action.resId != null) { + showThemedToast( + this@NoteTypeFieldEditor, + getString(state.action.resId), + true, + ) + } + finish() + } + NoteTypeFieldEditorState.Action.None -> { } + } + if (state.action != NoteTypeFieldEditorState.Action.None) { + viewModel.resetAction() + } } } } @@ -211,16 +243,12 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.notetype_field_editor, menu) - - menu.findItem(R.id.notetype_field_note).setOnMenuItemClickListener { - AlertDialog - .Builder(this) - .show { - setIcon(R.drawable.ic_dialog_info) - title(R.string.model_field_editor_note_title) - message(R.string.model_field_editor_note_description) - positiveButton(R.string.dialog_ok) - } + menu.findItem(R.id.action_save).setOnMenuItemClickListener { + val unsavedChange = adapter.unsavedChange + if (unsavedChange != null) { + viewModel.updateByUuid(unsavedChange) + } + viewModel.requestSaveAndClose() true } return true @@ -231,56 +259,66 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field // ---------------------------------------------------------------------------- /** - * Clean the input field or explain why it's rejected - * @param name the input - * @return The value to use, or null in case of failure + * shows a snackbar with the recent change label and an undo action */ - private fun uniqueName(name: String): String? { - var input = - name - .replace("[\\n\\r{}:\"]".toRegex(), "") - // The number of #, ^, /, space, tab, starting the input - var offset = 0 - while (offset < input.length) { - if (!listOf('#', '^', '/', ' ', '\t').contains(input[offset])) { - break + fun showUndoSnackbar(message: String) { + showSnackbar(message) { + setAnchorView(findViewById(R.id.btn_add)) + isAnchorViewLayoutListenerEnabled = true + setAction(R.string.undo) { + launchCatchingTask { + viewModel.undo() + } } - offset++ } - input = input.substring(offset).trim() - if (input.isEmpty()) { - showThemedToast( - this, - resources.getString(R.string.toast_empty_name), - true, - ) - return null + } + + /** + * shows a dialog to discard the changes + */ + fun showDiscardChangesDialog() { + DiscardChangesDialog.showDialog( + this, + positiveMethod = { + viewModel.requestDiscardChangesAndClose(true) + }, + ) + } + + /** + * shows a dialog to save the changes + */ + fun showSaveChangesDialog( + isNotUndoable: Boolean, + schemaChanges: Boolean, + ) { + val save: () -> Unit = { + launchCatchingTask { + if (!schemaChanges || userAcceptsSchemaChange()) { + viewModel.requestSaveAndClose(true) + } + } } - if (viewModel.state.value.fields - .any { input == it.name } - ) { - showThemedToast( - this, - resources.getString(R.string.toast_duplicate_field), - true, - ) - return null + if (isNotUndoable) { + val confirmationDialog = + ConfirmationDialog().apply { + setArgs(this@NoteTypeFieldEditor.getString(R.string.model_field_editor_save_not_undoable)) + setConfirm { + save() + } + } + showDialogFragment(confirmationDialog) + } else { + save() } - return input } - /* + /** * Creates a dialog to create a field */ private fun addFieldDialog() { - val addFieldDialog = AddNewNoteTypeField(this) - addFieldDialog.showAddNewNoteTypeFieldDialog { name -> - launchCatchingTask { - val validName = uniqueName(name) - val isConfirmed = validName != null && userAcceptsSchemaChange() - if (!isConfirmed) return@launchCatchingTask - viewModel.add(validName) - } + AddNewNoteTypeField(this).showAddNewNoteTypeFieldDialog { name -> + viewModel.add(name = name) } } @@ -333,46 +371,6 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field showDialogFragment(LocaleSelectionDialog.newInstance(position, locale)) } - /** - * Sets the Locale Hint of the field to the provided value. - * This allows some keyboard (GBoard) to change language - * @param position the position of the field - * @param selectedLocale the selected locale - */ - private fun addFieldLocaleHint( - position: Int, - selectedLocale: Locale?, - ) { - viewModel.languageHint(position, selectedLocale) - val format = - if (selectedLocale != null) { - getString( - R.string.model_field_editor_language_hint_dialog_success_result, - selectedLocale.displayName, - ) - } else { - getString(R.string.model_field_editor_language_hint_dialog_cleared_result) - } - showSnackbar(format, Snackbar.LENGTH_SHORT) - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @Throws(ConfirmModSchemaException::class) - fun addField(name: String) { - val fieldName = uniqueName(name) ?: return - viewModel.add(fieldName) - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @Throws(ConfirmModSchemaException::class) - fun renameField( - position: Int, - name: String, - ) { - val fieldLabel = uniqueName(name) ?: return - viewModel.rename(position, fieldLabel) - } - companion object { const val EXTRA_NOTETYPE_NAME = "extra_notetype_name" const val EXTRA_NOTETYPE_ID = "extra_notetype_id" @@ -397,10 +395,22 @@ private class NoteFieldAdapter( holder.bind(getItem(position)) } + override fun onViewDetachedFromWindow(holder: NoteFieldViewHolder) { + holder.detached() + } + override fun onViewRecycled(holder: NoteFieldViewHolder) { holder.recycled() } + private var _unsavedChange: NoteTypeFieldRowData? = null + + val unsavedChange: NoteTypeFieldRowData? get() { + val change = _unsavedChange + _unsavedChange = null + return change + } + companion object { private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { @@ -438,26 +448,58 @@ private class NoteFieldAdapter( listener.onLocaleChangeRequested(position) } } - fieldEdit.setOnFocusChangeListener { v, hasFocus -> - if (!hasFocus) { - v.hideKeyboard() + fieldEdit.apply { + setOnFocusChangeListener { v, _ -> + moveCursorToEnd() + hideKeyboard() val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { - val name = (v as TextInputEditText).text?.toString().orEmpty() - val oldName = (bindingAdapter as NoteFieldAdapter).getItem(position).name - if (name.isNotBlank() && name != oldName) { + val name = (v as TextInputEditText).text?.toString() + if (!name.isNullOrBlank()) { listener.onNameChanged(position, name) + _unsavedChange = null } else { - v.setText(oldName) + setText((bindingAdapter as NoteFieldAdapter).getItem(bindingAdapterPosition).name) } } } - } - fieldEdit.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - root.requestFocus() + setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + root.requestFocus() + } + false } - true + addTextChangedListener( + object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + end: Int, + ) { + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + count: Int, + end: Int, + ) { + if (bindingAdapterPosition != RecyclerView.NO_POSITION) { + val unsavedChange = + (bindingAdapter as NoteFieldAdapter) + .getItem( + bindingAdapterPosition, + ).copy( + name = s?.toString().orEmpty(), + ) + _unsavedChange = unsavedChange + } + } + + override fun afterTextChanged(s: Editable?) { } + }, + ) } fieldEditLayout.setEndIconOnClickListener { val position = bindingAdapterPosition @@ -472,18 +514,20 @@ private class NoteFieldAdapter( fun bind(item: NoteTypeFieldRowData) { binding.apply { - fieldEdit.setText(item.name) + if (fieldEdit.text.toString() != item.name) { + fieldEdit.setText(item.name) + } + fieldEditLayout.isEndIconVisible = false fieldSortButton.isChecked = item.isOrder fieldLanguageButton.isChecked = item.locale != null } } + fun detached() { + binding.fieldEdit.clearFocus() + } + fun recycled() { - binding.fieldEdit.apply { - setText("") - clearFocus() - } - binding.fieldEditLayout.clearFocus() binding.root.translationX = 0f } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt index 049b5bb85528..0fd4aab22501 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorState.kt @@ -1,5 +1,37 @@ package com.ichi2.anki.notetype.fieldeditor +import androidx.annotation.StringRes +import com.ichi2.anki.notetype.ManageNoteTypesState + data class NoteTypeFieldEditorState( val fields: List, -) + val action: Action = Action.None, +) { + sealed interface Action { + data class Undoable( + @StringRes val resId: Int, + val formatArgs: List = emptyList(), + ) : Action + + data class Rejected( + @StringRes val resId: Int, + ) : Action + + data class Error( + val e: ManageNoteTypesState.ReportableException, + ) : Action + + data class SaveRequested( + val isNotUndoable: Boolean, + val isSchemaChanges: Boolean, + ) : Action + + object DiscardRequested : Action + + data class Close( + @StringRes val resId: Int? = null, + ) : Action + + object None : Action + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index 8c133422b43a..cd05ae593823 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -1,125 +1,183 @@ package com.ichi2.anki.notetype.fieldeditor +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import anki.collection.OpChanges -import anki.collection.copy import anki.notetypes.Notetype +import anki.notetypes.NotetypeKt import anki.notetypes.copy import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.anki.R import com.ichi2.anki.common.utils.ext.indexOfOrNull import com.ichi2.anki.libanki.Collection -import com.ichi2.anki.libanki.NoteTypeId -import com.ichi2.anki.libanki.UndoStepCounter -import com.ichi2.anki.libanki.backend.BackendUtils.toJsonBytes +import com.ichi2.anki.libanki.NotetypeJson +import com.ichi2.anki.libanki.Notetypes import com.ichi2.anki.libanki.getNotetype import com.ichi2.anki.libanki.updateNotetype +import com.ichi2.anki.notetype.ManageNoteTypesState.ReportableException import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditor.Companion.EXTRA_NOTETYPE_ID +import com.ichi2.anki.notetype.fieldeditor.NoteTypeFieldEditorViewModel.Companion.NO_POSITION import com.ichi2.anki.observability.undoableOp import com.ichi2.anki.servicelayer.LanguageHint +import com.ichi2.anki.servicelayer.LanguageHintService import com.ichi2.anki.servicelayer.LanguageHintService.languageHint +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.ankiweb.rsdroid.BackendException +import org.jetbrains.annotations.Contract import timber.log.Timber -import java.util.Collections import java.util.UUID +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract class NoteTypeFieldEditorViewModel( savedStateHandle: SavedStateHandle, ) : ViewModel() { - val noteTypeId = savedStateHandle.get(EXTRA_NOTETYPE_ID) ?: 0 - var undoStepCounter: UndoStepCounter? = null - private val _state by lazy { - MutableStateFlow( - NoteTypeFieldEditorState( - fields = emptyList(), - ), + private val ntid = savedStateHandle.get(EXTRA_NOTETYPE_ID)!! + private val fieldsEditOperationStack = + savedStateHandle.getMutableStateFlow( + KEY_FIELD_EDIT_OPERATION, + listOf(), ) - } + + /** + * Checks if there are unsaved undoable changes or no changes + */ + private val isNotUndoable get() = fieldsEditOperationStack.value.any { !it.isUndoable } || fieldsEditOperationStack.value.isEmpty() + + private val _state = MutableStateFlow(NoteTypeFieldEditorState(fields = emptyList())) val state: StateFlow = _state.asStateFlow() init { viewModelScope.launch { - forceRefresh() + refresh() } } /** - * Create new field with the given name + * Creates new field with the given name + * @param position the position of the new field or [NO_POSITION] to add to the end * @param name the name of the field, which must be unique, not empty, allowed to the notetype */ - fun add(name: String) { - Timber.d("doInBackgroundAddField") - viewModelScope.launch { - mergeUndoableOp { - safeAddField(noteTypeId, name) - .onSuccess { smartRefresh() } - .onFailure { forceRefresh() } - .getOrDefault(OpChanges.getDefaultInstance()) - } - } + fun add( + position: Int = NO_POSITION, + name: String, + ) { + uniqueName(name = name).fold( + onSuccess = { validName -> + val position = if (position == NO_POSITION) state.value.fields.lastIndex + 1 else position + _state.update { oldValue -> + val fields = oldValue.fields.toMutableList() + fields.temporaryAdd(position, validName) + val action = NoteTypeFieldEditorState.Action.Undoable(R.string.model_field_editor_add_success_result, listOf(validName)) + return@update oldValue.copy(fields = fields.toList(), action = action) + } + val operation = NoteTypeFieldOperation.Add(position, validName) + fieldsEditOperationStack.update { + val mutableStack = it.toMutableList() + mutableStack.add(operation) + return@update mutableStack.toList() + } + }, + onFailure = { resId -> + val action = NoteTypeFieldEditorState.Action.Rejected(resId) + _state.value = state.value.copy(action = action) + }, + ) } /** - * Rename the existing field with the given name + * Renames the existing field with the given name + * * @param position the position of the field to rename - * @param name the name of the field, which must be unique, not empty, addowed to the notetype + * @param name the name of the field */ fun rename( position: Int, name: String, ) { - Timber.d("doInBackgroundRenameField") - viewModelScope.launch { - mergeUndoableOp { - safeRenameField(noteTypeId, position, name) - .onSuccess { smartRefresh() } - .onFailure { forceRefresh() } - .getOrDefault(OpChanges.getDefaultInstance()) - } - } + val oldName = state.value.fields[position].name + if (oldName == name) return + uniqueName(name).fold( + onSuccess = { validName -> + val oldName = state.value.fields[position].name + _state.update { oldValue -> + val fields = oldValue.fields.toMutableList() + fields.temporaryRename(position, validName) + val action = + NoteTypeFieldEditorState.Action.Undoable( + R.string.model_field_editor_rename_success_result, + arrayListOf(oldName, validName), + ) + return@update oldValue.copy(fields = fields.toList(), action = action) + } + + val operation = NoteTypeFieldOperation.Rename(position, oldName, validName) + fieldsEditOperationStack.update { stack -> + val mutableStack = stack.toMutableList() + mutableStack.add(operation) + return@update mutableStack.toList() + } + }, + onFailure = { resId -> + _state.update { oldValue -> + val fields = oldValue.fields.toMutableList() + fields.temporaryRefresh(position) + val action = NoteTypeFieldEditorState.Action.Rejected(resId) + return@update oldValue.copy(fields = fields.toList(), action = action) + } + }, + ) } /** - * Delete the existing field with the given name + * Deletes the existing field with the given name * @param position the position of the field to delete */ fun delete(position: Int) { - Timber.d("doInBackgroundDeleteField") - viewModelScope.launch { - mergeUndoableOp { - safeDeleteField(noteTypeId, position) - .onSuccess { smartRefresh() } - .onFailure { forceRefresh() } - .getOrDefault(OpChanges.getDefaultInstance()) - } + val isLast = position == state.value.fields.lastIndex + + if (position == 0 && isLast) { + val action = NoteTypeFieldEditorState.Action.Rejected(R.string.toast_last_field) + _state.value = state.value.copy(action = action) + return } - } - /** - * Set the existing field as the order field - * @param position the position of the target field - */ - fun changeSort(position: Int) { - Timber.d("doInBackgroundChangeSortField") - viewModelScope.launch { - mergeUndoableOp { - safeChangeSort(noteTypeId, position) - .onSuccess { smartRefresh() } - .onFailure { forceRefresh() } - .getOrDefault(OpChanges.getDefaultInstance()) + val fieldData = state.value.fields[position] + _state.update { oldValue -> + val fields = oldValue.fields.toMutableList() + fields.temporaryDelete(position) + if (isLast) { + fields.temporaryChangeSort(position - 1) } + val action = + NoteTypeFieldEditorState.Action.Undoable( + R.string.model_field_editor_delete_success_result, + arrayListOf(fieldData.name), + ) + return@update oldValue.copy(fields = fields.toList(), action = action) + } + val operation = NoteTypeFieldOperation.Delete(position, fieldData, isLast) + fieldsEditOperationStack.update { + val mutableStack = it.toMutableList() + mutableStack.add(operation) + return@update mutableStack.toList() } } /** - * Move the existing field to the given position in ViewState + * Moves the existing field to the given position in ViewState * - * This method does NOT persist changes to the database + * This method does NOT record changes + * reposition() must be called after calling visuallyReposition() before calling any other methods * * @param oldPosition the current position of the target field * @param newPosition the new position of the target field @@ -131,17 +189,19 @@ class NoteTypeFieldEditorViewModel( ) { _state.update { oldValue -> val fields = oldValue.fields.toMutableList() - Collections.swap(fields, oldPosition, newPosition) - oldValue.copy(fields = fields.toList()) + fields.temporaryReposition(oldPosition, newPosition) + val action = NoteTypeFieldEditorState.Action.None + return@update oldValue.copy(fields = fields.toList(), action = action) } } /** - * Move the existing field to the given position in ViewState + * Moves the existing field to the given position in ViewState * - * This method DOES persist changes to the database + * This method DOES record changes + * visuallyReposition() must be called before calling visuallyReposition() * - * @param oldPosition the current position of the target field + * @param oldPosition the position of the target field before repositioning it * @param newPosition the new position of the target field * @see NoteTypeFieldEditorViewModel.visuallyReposition */ @@ -149,248 +209,603 @@ class NoteTypeFieldEditorViewModel( oldPosition: Int, newPosition: Int, ) { - Timber.d("doInBackgroundRepositionField") - Timber.i("Repositioning field from %d to %d", oldPosition, newPosition) - viewModelScope.launch { - mergeUndoableOp { - safeReposition(noteTypeId, oldPosition, newPosition) - .onSuccess { smartRefresh() } - .onFailure { forceRefresh() } - .getOrDefault(OpChanges.getDefaultInstance()) - } + // fields has been repositioned by visuallyReposition(). + _state.update { oldValue -> + val name = oldValue.fields[newPosition].name + val action = + NoteTypeFieldEditorState.Action.Undoable( + R.string.model_field_editor_reposition_success_result, + arrayListOf( + name, + oldPosition + 1, + newPosition + 1, + ), + ) + return@update oldValue.copy(action = action) + } + val operation = NoteTypeFieldOperation.Reposition(oldPosition, newPosition) + fieldsEditOperationStack.update { + val mutableStack = it.toMutableList() + mutableStack.add(operation) + return@update mutableStack.toList() } } /** - * Set a [Locale] as the keyboard hint for the field + * Sets the existing field as the order field + * @param position the position of the target field + */ + fun changeSort(position: Int) { + val fields = state.value.fields + val oldPosition = fields.indexOfFirst { it.isOrder } + val name = fields[position].name + if (oldPosition == position) { + val action = NoteTypeFieldEditorState.Action.None + _state.value = state.value.copy(action = action) + return + } + _state.update { oldValue -> + val list = fields.toMutableList() + list.temporaryChangeSort(position) + val action = NoteTypeFieldEditorState.Action.Undoable(R.string.model_field_editor_sort_field_success_result, arrayListOf(name)) + return@update oldValue.copy(fields = list.toList(), action = action) + } + val operation = NoteTypeFieldOperation.ChangeSort(oldPosition, position) + fieldsEditOperationStack.update { + val mutableStack = it.toMutableList() + mutableStack.add(operation) + return@update mutableStack.toList() + } + } + + /** + * Sets a [LanguageHint] as the keyboard hint for the field * * @param position the position of the target field - * @param locale the [Locale] to set, or null to clear the hint + * @param locale the [LanguageHint] to set, or null to clear the hint * @see LanguageHintService */ - fun languageHint( + fun setLanguageHint( position: Int, locale: LanguageHint?, ) { - viewModelScope.launch { - mergeUndoableOp { - safeChangeLanguageHint(noteTypeId, position, locale) - .onSuccess { smartRefresh() } - .onFailure { forceRefresh() } - .getOrDefault(OpChanges.getDefaultInstance()) - .copy { - notetype = true - } + val oldLocale = state.value.fields[position].locale + if (oldLocale == locale) { + val action = NoteTypeFieldEditorState.Action.None + _state.value = state.value.copy(action = action) + return + } + _state.update { oldValue -> + val name = oldValue.fields[position].name + val fields = oldValue.fields.toMutableList() + fields.temporarySetLanguageHint(position, locale) + val action = + if (locale != null) { + NoteTypeFieldEditorState.Action.Undoable( + R.string.model_field_editor_language_hint_success_result, + listOf(locale.displayName, name), + ) + } else { + NoteTypeFieldEditorState.Action.Undoable(R.string.model_field_editor_language_hint_cleared_success_result, listOf(name)) + } + return@update oldValue.copy(fields = fields.toList(), action = action) + } + val operation = NoteTypeFieldOperation.LanguageHint(position, oldLocale, locale) + fieldsEditOperationStack.update { + val mutableStack = it.toMutableList() + mutableStack.add(operation) + return@update mutableStack.toList() + } + } + + /** + * Updates the field with the given row data referring to the uuid of the data + * @param rowData the row data of the field + * @param position the new position of the field or [NO_POSITION] to add to the end or not to move the field + */ + fun updateByUuid( + rowData: NoteTypeFieldRowData, + position: Int = NO_POSITION, + ) { + val fieldIndex = state.value.fields.indexOfOrNull { it.uuid == rowData.uuid } + val isNew = fieldIndex == null + val position = + if (position == NO_POSITION) { + fieldIndex + ?: (state.value.fields.lastIndex + 1) + } else { + position } + if (isNew) { + add(position, rowData.name) + val fields = state.value.fields.toMutableList() + // Update the uuid of the new field + // This is a new field so it has a unique uuid + fields[position] = fields[position].copy(uuid = rowData.uuid) + _state.value = state.value.copy(fields = state.value.fields.toList()) } + + val field = state.value.fields[position] + if (field.name != rowData.name) rename(position, rowData.name) + if (!isNew && fieldIndex != position) { + visuallyReposition(fieldIndex, position) + reposition(fieldIndex, position) + } + if (field.isOrder != rowData.isOrder) changeSort(position) + if (field.locale != rowData.locale) setLanguageHint(position, rowData.locale) } /** - * Refresh the data of the target field on the given position - * - * This is used to clear user's change which is not applied to the database - * @param position the position of the target field - * @see NoteTypeFieldEditorViewModel.smartRefresh() - * @see NoteTypeFieldEditorViewModel.forceRefresh() + * Obtains the field list from [Collection] and refresh the state */ - fun forceRefresh(position: Int) { + fun refresh() { viewModelScope.launch { - _state.update { - val list = it.fields.toMutableList() - list.apply { - this[position] = this[position].copy(uuid = UUID.randomUUID().toString()) - } - it.copy(fields = list.toList()) + _state.update { oldValue -> + val data = withCol { obtainData() } + val action = NoteTypeFieldEditorState.Action.None + return@update oldValue.copy(fields = data, action = action) + } + } + fieldsEditOperationStack.update { + val mutableStack = it.toMutableList() + mutableStack.clear() + return@update mutableStack.toList() + } + } + + private fun MutableList.temporaryAdd( + position: Int = NO_POSITION, + name: String, + ) { + val newField = NoteTypeFieldRowData(name = name) + if (position != NO_POSITION) { + this.add(position, newField) + } else { + this.add(newField) + } + } + + private fun MutableList.temporaryRename( + position: Int, + newName: String, + ) { + val field = this[position] + this[position] = field.copy(name = newName) + } + + private fun MutableList.temporaryDelete(position: Int) { + removeAt(position) + } + + private fun MutableList.temporaryReposition( + oldPosition: Int, + newPosition: Int, + ) { + val field = this.removeAt(oldPosition) + this.add(newPosition, field) + } + + private fun MutableList.temporaryChangeSort(position: Int) { + for (i in 0...temporarySetLanguageHint( + position: Int, + locale: LanguageHint?, + ) { + val field = this[position] + this[position] = field.copy(locale = locale) + } + + private fun MutableList.temporaryRefresh(position: Int) = temporaryUpdateUuid(position) + + private fun MutableList.temporaryUpdateUuid( + position: Int, + uuid: String = UUID.randomUUID().toString(), + ) { + val field = this[position] + this[position] = field.copy(uuid = uuid) + } + /** - * Obtain the data from the database and update the state without the identifying data of the data changing + * Cleans the input field or explain why it's rejected + * @param name the input + * @return the result UniqueNameResult.Success which contains the unique name or UniqueNameResult.Failure which contains string resource id of the reason why it's rejected * - * This tries to keep the identifying data of the data of the fields if possible to avoid innecessary UI refreshes. - * This can be invoked ONLY IF the data is NOT corrupted - * and ONLY AFTER you updated the database successfully. - * @see NoteTypeFieldEditorViewModel.forceRefresh(Int) - * @see NoteTypeFieldEditorViewModel.forceRefresh() */ - fun smartRefresh() { - viewModelScope.launch { - val oldData = state.value.fields - val newData = withCol { obtainData() } - val oldDataNameList = oldData.map { it.name } - val newNamedDataIndex = newData.indexOfOrNull { !oldDataNameList.contains(it.name) } - val isRenamed = oldData.size == newData.size && newNamedDataIndex != null - val updateData = - newData.mapIndexed { index, new -> - if (isRenamed && index == newNamedDataIndex) { - // when renamed, index is not changed. - val renamedDataIndex = newNamedDataIndex - // succeed to the previous uuid referring to index - new.copy(uuid = oldData[renamedDataIndex].uuid) - } else { - // succeed to the previous uuid referring to name - new.copy(uuid = oldData.firstOrNull { it.name == new.name }?.uuid ?: UUID.randomUUID().toString()) - } - } - _state.update { oldState -> oldState.copy(fields = updateData) } + private fun uniqueName(name: String): UniqueNameResult { + var input = + name + .replace("[\\n\\r{}:\"]".toRegex(), "") + // The number of #, ^, /, space, tab, starting the input + var offset = 0 + while (offset < input.length) { + if (!listOf('#', '^', '/', ' ', '\t').contains(input[offset])) { + break + } + offset++ + } + input = input.substring(offset).trim() + if (input.isEmpty()) { + return UniqueNameResult.Failure(R.string.toast_empty_name) + } + if (state.value.fields.any { it.name == input }) { + return UniqueNameResult.Failure(R.string.toast_duplicate_field) } + return UniqueNameResult.Success(input) } /** - * Obtain the data from the database and update the state - * This completely overrides the state. - * This is useful when something went wrong or you want to update the whole data. - * @see NoteTypeFieldEditorViewModel.forceRefresh(Int) - * @see NoteTypeFieldEditorViewModel.smartRefresh() + * Undo the last unsaved change */ - fun forceRefresh() { - viewModelScope.launch { + fun undo() { + val undoOperation = fieldsEditOperationStack.value.lastOrNull() + + if (undoOperation == null) { _state.update { oldValue -> - val data = withCol { obtainData() } - oldValue.copy(fields = data) + val e = ReportableException(IllegalStateException("Undo operation is null")) + oldValue.copy(action = NoteTypeFieldEditorState.Action.Error(e)) } + return + } + _state.update { oldValue -> + val list = oldValue.fields.toMutableList() + list.apply { + when (undoOperation) { + is NoteTypeFieldOperation.Add -> + temporaryDelete(undoOperation.position) + is NoteTypeFieldOperation.Rename -> + temporaryRename(undoOperation.position, undoOperation.oldName) + is NoteTypeFieldOperation.Reposition -> + temporaryReposition(undoOperation.newPosition, undoOperation.oldPosition) + is NoteTypeFieldOperation.ChangeSort -> + temporaryChangeSort(undoOperation.oldPosition) + is NoteTypeFieldOperation.LanguageHint -> + temporarySetLanguageHint(undoOperation.position, undoOperation.oldLocale) + is NoteTypeFieldOperation.Delete -> { + temporaryAdd(undoOperation.position, undoOperation.fieldData.name) + temporaryUpdateUuid(undoOperation.position, undoOperation.fieldData.uuid) + temporarySetLanguageHint(undoOperation.position, undoOperation.fieldData.locale) + if (undoOperation.isLast && undoOperation.fieldData.isOrder) { + temporaryChangeSort(undoOperation.position) + } + } + } + } + val action = NoteTypeFieldEditorState.Action.None + return@update oldValue.copy(fields = list.toList(), action = action) + } + fieldsEditOperationStack.update { + val mutableStack = it.toMutableList() + mutableStack.removeLastOrNull() + return@update mutableStack.toList() } } - private suspend fun mergeUndoableOp(block: Collection.() -> T) { - if (undoStepCounter == null) { - withCol { - undoStepCounter = addCustomUndoEntry(tr.actionsUpdateNotetype()) + /** + * Checks unsaved changes and requests to show discard confirmation dialog if changes not saved, + * otherwise close the editor + * @param force if true, discard changes even if not saved + */ + fun requestDiscardChangesAndClose(force: Boolean = false) { + Timber.d("requestDiscardChangesAndClose") + val action = + when { + fieldsEditOperationStack.value.isEmpty() -> NoteTypeFieldEditorState.Action.Close() + force -> NoteTypeFieldEditorState.Action.Close(R.string.model_field_editor_discard_success_result) + else -> NoteTypeFieldEditorState.Action.DiscardRequested + } + _state.value = state.value.copy(action = action) + } + + /** + * Checks unsaved changes and requests to show save confirmation dialog if changes not saved, + * otherwise close the editor + * @param force if true, discard changes even if not saved + */ + fun requestSaveAndClose(force: Boolean = false) { + Timber.d("requestSaveAndClose") + val isSchemaChange = fieldsEditOperationStack.value.any { it.isSchemaChange } + if (fieldsEditOperationStack.value.isEmpty()) { + val action = NoteTypeFieldEditorState.Action.Close() + _state.value = state.value.copy(action = action) + } else if (force || (!isSchemaChange && !isNotUndoable)) { + viewModelScope.launch { + val action = + save().fold( + onSuccess = { NoteTypeFieldEditorState.Action.Close(R.string.model_field_editor_save_success_result) }, + onFailure = { NoteTypeFieldEditorState.Action.Error(ReportableException(it, it !is BackendException)) }, + ) + _state.value = state.value.copy(action = action) } + } else { + val action = NoteTypeFieldEditorState.Action.SaveRequested(isNotUndoable, isSchemaChange) + _state.value = state.value.copy(action = action) } - undoableOp { - block() + } + + /** + * Resets the current action of the state + * + * used when the current action is consumed in the side of UI + */ + fun resetAction() { + val action = NoteTypeFieldEditorState.Action.None + _state.value = state.value.copy(action = action) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + suspend fun save() = + withContext(Dispatchers.IO) { + return@withContext if (isNotUndoable) { + saveIrreversible() + } else { + saveReversible() + }.map { } + } + + /** + * Saves changes and notify change subscribers of them + * + * Don't use this if [NoteTypeFieldOperation.LanguageHint] is included in fieldsEditOperationStack + * because it cannot be undone and clear Anki's change history. + * @see saveIrreversible + */ + private suspend fun saveReversible() = + runCatching { + undoableOp { + val notetype = + getNotetype(ntid).copy { + fieldsEditOperationStack.value.forEach { operation -> + when (operation) { + is NoteTypeFieldOperation.Add -> + addField(operation.name) + is NoteTypeFieldOperation.Rename -> + renameField(operation.position, operation.newName) + is NoteTypeFieldOperation.Delete -> + deleteField(operation.position) + is NoteTypeFieldOperation.Reposition -> + repositionField(operation.oldPosition, operation.newPosition) + is NoteTypeFieldOperation.ChangeSort -> + changeSortField(operation.newPosition) + is NoteTypeFieldOperation.LanguageHint -> + throw IllegalStateException("Language hint is not reversible.") + } + } + } + updateNotetype(notetype) + } } - val step = undoStepCounter ?: return - undoableOp { - mergeUndoEntries(step) + + /** + * Saves changes + * + * Use this if [NoteTypeFieldOperation.LanguageHint] is included in fieldsEditOperationStack + * because it cannot be undone and clear Anki's change history. + * @see saveReversible + */ + private suspend fun saveIrreversible() = + runCatching { + withCol { + notetypes.apply { + val notetype = get(ntid)!! + fieldsEditOperationStack.value.forEach { operation -> + when (operation) { + is NoteTypeFieldOperation.Add -> + addFieldAlternative( + notetype, + operation.name, + ) + is NoteTypeFieldOperation.Rename -> + renameFieldAlternative( + notetype, + operation.position, + operation.newName, + ) + is NoteTypeFieldOperation.Delete -> + deleteFieldAlternative( + notetype, + operation.position, + ) + is NoteTypeFieldOperation.Reposition -> + repositionAlternative( + notetype, + operation.oldPosition, + operation.newPosition, + ) + is NoteTypeFieldOperation.ChangeSort -> + changeSortFieldAlternative( + notetype, + operation.newPosition, + ) + is NoteTypeFieldOperation.LanguageHint -> + setLanguageHintAlternative( + notetype, + operation.position, + operation.newLocale, + ) + } + } + } + } } - } + /** + * Obtains the field list from [Collection] + * @return a list of [NoteTypeFieldRowData] + */ private fun Collection.obtainData(): List { - val langugageMap = - notetypes.get(noteTypeId)!!.fields.associate { + val languageMap = + notetypes.get(ntid)!!.fields.associate { it.name to it.languageHint } - return getNotetype(noteTypeId).run { + return getNotetype(ntid).run { val sortF = config.sortFieldIdx fieldsList.mapIndexed { index, it -> NoteTypeFieldRowData( name = it.name, isOrder = index == sortF, - locale = langugageMap.getOrDefault(it.name, null), + locale = languageMap.getOrDefault(it.name, null), ) } } } - private fun Collection.safeAddField( - ntid: NoteTypeId, + private fun NotetypeKt.Dsl.addField(newName: String) { + Timber.d("doInBackgroundAddField") + fields.apply { + val field = + Notetype.Field + .newBuilder() + .setName(newName) + .build() + add(field) + } + } + + private fun NotetypeKt.Dsl.renameField( + position: Int, newName: String, - ) = runCatching { - val notetype = - getNotetype(ntid).copy { - fields.apply { - val field = - Notetype.Field - .newBuilder() - .setName(newName) - .build() - add(field) - } - } - updateNotetype(notetype) + ) { + Timber.d("doInBackgroundRenameField") + fields.apply { + val field = this[position] + this[position] = field.copy { name = newName } + } + } + + private fun NotetypeKt.Dsl.deleteField(position: Int) { + Timber.d("doInBackgroundDeleteField") + fields.apply { + val list = this.toMutableList().apply { removeAt(position) }.toList() + clear() + addAll(list) + } + } + + private fun NotetypeKt.Dsl.repositionField( + oldPosition: Int, + newPosition: Int, + ) { + Timber.d("doInBackgroundRepositionField") + Timber.i("Repositioning field from %d to %d", oldPosition, newPosition) + fields.apply { + val list = + toMutableList() + .apply { + val field = this.removeAt(oldPosition) + this.add(newPosition, field) + }.toList() + clear() + addAll(list) + } + } + + private fun NotetypeKt.Dsl.changeSortField(position: Int) { + Timber.d("doInBackgroundChangeSortField") + config = config.copy { sortFieldIdx = position } + } + + private fun Notetypes.addFieldAlternative( + notetype: NotetypeJson, + name: String, + ) { + Timber.d("doInBackgroundAddFieldAlternative") + val field = newField(name) + addField(notetype, field) + save(notetype) } - private fun Collection.safeRenameField( - ntid: NoteTypeId, + private fun Notetypes.renameFieldAlternative( + notetype: NotetypeJson, position: Int, newName: String, - ) = runCatching { - val notetype = - getNotetype(ntid).copy { - fields.apply { - val field = this[position] - this[position] = field.copy { name = newName } - } - } - updateNotetype(notetype) + ) { + Timber.d("doInBackgroundRenameFieldAlternative") + val field = notetype.getField(position) + renameField(notetype, field, newName) + save(notetype) } - private fun Collection.safeDeleteField( - ntid: NoteTypeId, + private fun Notetypes.deleteFieldAlternative( + notetype: NotetypeJson, position: Int, - ) = runCatching { - val notetype = - getNotetype(ntid).copy { - fields.apply { - val list = this.toMutableList().apply { removeAt(position) }.toList() - clear() - addAll(list) - } - } - updateNotetype(notetype) + ) { + Timber.d("doInBackgroundDeleteFieldAlternative") + val field = notetype.getField(position) + removeField(notetype, field) + save(notetype) } - private fun Collection.safeReposition( - ntid: NoteTypeId, + private fun Notetypes.repositionAlternative( + notetype: NotetypeJson, oldPosition: Int, newPosition: Int, - ) = runCatching { - val notetype = - getNotetype(ntid).copy { - fields.apply { - val list = - this - .toMutableList() - .apply { - val field = this.removeAt(oldPosition) - this.add(newPosition, field) - }.toList() - clear() - addAll(list) - } - } - updateNotetype(notetype) + ) { + Timber.d("doInBackgroundRepositionFieldAlternative") + Timber.i("Repositioning field from %d to %d", oldPosition, newPosition) + val field = notetype.getField(oldPosition) + repositionField(notetype, field, newPosition) + save(notetype) } - private fun Collection.safeChangeSort( - ntid: NoteTypeId, + private fun Notetypes.changeSortFieldAlternative( + notetype: NotetypeJson, position: Int, - ) = runCatching { - val notetype = - getNotetype(ntid).copy { - val newConfig = configOrNull ?: Notetype.Config.newBuilder().build() - config = newConfig.copy { sortFieldIdx = position } - } - updateNotetype(notetype) + ) { + Timber.d("doInBackgroundChangeSortFieldAlternative") + setSortIndex(notetype, position) + save(notetype) } - private fun Collection.safeChangeLanguageHint( - ntid: NoteTypeId, + private fun Notetypes.setLanguageHintAlternative( + notetype: NotetypeJson, position: Int, - selectedLocale: LanguageHint?, - ) = runCatching { - // notetypes.save(notetype) (which call addOrUpdateNotetype()) will delete OpChanges, so we need to convert Field to Notetype.Field - val notetypeJson = notetypes.get(ntid)!! - val field = notetypeJson.getField(position) - field.languageHint = selectedLocale - - val notetype = - getNotetype(ntid).copy { - fields.apply { - val list = - this - .toMutableList() - .apply { - this[position] = Notetype.Field.parseFrom(toJsonBytes(field)) - }.toList() - clear() - addAll(list) - } + locale: LanguageHint?, + ) { + Timber.i("Set field locale to %s", locale) + LanguageHintService.setLanguageHintForField(this, notetype, position, locale) + } + + private companion object { + private const val KEY_FIELD_EDIT_OPERATION = "key_field_edit_operation" + private const val NO_POSITION = -1 + } + + private sealed class UniqueNameResult { + data class Success( + /** + * The unique name of the field + */ + val name: String, + ) : UniqueNameResult() + + data class Failure( + /** + * The string resource id of the reason why the name is rejected + */ + @StringRes val resId: Int, + ) : UniqueNameResult() + + @OptIn(ExperimentalContracts::class) + @Contract + fun fold( + onSuccess: (String) -> Unit, + onFailure: (resId: Int) -> Unit, + ) { + contract { + callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE) + callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE) + } + when (this) { + is Success -> onSuccess(name) + is Failure -> onFailure(resId) } - Timber.i("Set field locale to %s", selectedLocale) - updateNotetype(notetype) + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt new file mode 100644 index 000000000000..eb2f9742eeb8 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt @@ -0,0 +1,81 @@ +package com.ichi2.anki.notetype.fieldeditor + +import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import java.util.Locale + +@Parcelize +sealed interface NoteTypeFieldOperation : Parcelable { + val isUndoable: Boolean + val isSchemaChange: Boolean + + data class Add( + val position: Int, + val name: String, + ) : NoteTypeFieldOperation { + @IgnoredOnParcel + override val isUndoable = true + + @IgnoredOnParcel + override val isSchemaChange = true + } + + data class Rename( + val position: Int, + val oldName: String, + val newName: String, + ) : NoteTypeFieldOperation { + @IgnoredOnParcel + override val isUndoable = true + + @IgnoredOnParcel + override val isSchemaChange = false + } + + data class Delete( + val position: Int, + val fieldData: NoteTypeFieldRowData, + val isLast: Boolean, + ) : NoteTypeFieldOperation { + @IgnoredOnParcel + override val isUndoable = true + + @IgnoredOnParcel + override val isSchemaChange = false + } + + data class ChangeSort( + val oldPosition: Int, + val newPosition: Int, + ) : NoteTypeFieldOperation { + @IgnoredOnParcel + override val isUndoable = true + + @IgnoredOnParcel + override val isSchemaChange = true + } + + data class Reposition( + val oldPosition: Int, + val newPosition: Int, + ) : NoteTypeFieldOperation { + @IgnoredOnParcel + override val isUndoable = true + + @IgnoredOnParcel + override val isSchemaChange = true + } + + data class LanguageHint( + val position: Int, + val oldLocale: Locale?, + val newLocale: Locale?, + ) : NoteTypeFieldOperation { + @IgnoredOnParcel + override val isUndoable = false + + @IgnoredOnParcel + override val isSchemaChange = true + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt index e13c5a7cc9d4..c795099e3a70 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt @@ -1,11 +1,14 @@ package com.ichi2.anki.notetype.fieldeditor +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import java.util.Locale import java.util.UUID +@Parcelize data class NoteTypeFieldRowData( val uuid: String = UUID.randomUUID().toString(), val name: String, val isOrder: Boolean = false, val locale: Locale? = null, -) +) : Parcelable diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/LanguageHintService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/LanguageHintService.kt index b55b72cf1046..57e56c21e9a5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/LanguageHintService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/LanguageHintService.kt @@ -39,7 +39,7 @@ object LanguageHintService { notetypes: Notetypes, notetype: NotetypeJson, fieldPos: Int, - selectedLocale: Locale, + selectedLocale: Locale?, ) { val field = notetype.getField(fieldPos) field.languageHint = selectedLocale @@ -48,18 +48,6 @@ object LanguageHintService { Timber.i("Set field locale to %s", selectedLocale) } - fun clearLanguageHintForField( - notetypes: Notetypes, - notetype: NotetypeJson, - fieldPos: Int, - ) { - val field = notetype.getField(fieldPos) - field.languageHint = null - notetypes.save(notetype) - - Timber.i("Clear field locale") - } - fun compareLanguage( locale1: Locale, locale2: Locale, diff --git a/AnkiDroid/src/main/res/drawable/field_check.xml b/AnkiDroid/src/main/res/drawable/field_check.xml deleted file mode 100644 index 4c799f4a9192..000000000000 --- a/AnkiDroid/src/main/res/drawable/field_check.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/AnkiDroid/src/main/res/drawable/ic_edit_keyboard.xml b/AnkiDroid/src/main/res/drawable/ic_edit_keyboard.xml index 793828592307..b859dde6308f 100644 --- a/AnkiDroid/src/main/res/drawable/ic_edit_keyboard.xml +++ b/AnkiDroid/src/main/res/drawable/ic_edit_keyboard.xml @@ -2,7 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24"> + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + android:minHeight="?android:attr/listPreferredItemHeight"> + android:gravity="center"> . --> + xmlns:ankidroid="http://schemas.android.com/apk/res-auto"> + android:id="@+id/action_save" + android:icon="@drawable/ic_done_white" + android:title="@string/save" + ankidroid:showAsAction="always"/> \ No newline at end of file diff --git a/AnkiDroid/src/main/res/values-af/17-model-manager.xml b/AnkiDroid/src/main/res/values-af/17-model-manager.xml index 9f1a873bcb7e..ef6d35e0b67d 100644 --- a/AnkiDroid/src/main/res/values-af/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-af/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-am/17-model-manager.xml b/AnkiDroid/src/main/res/values-am/17-model-manager.xml index 4f3dfdbbb2dd..c550609bd88f 100644 --- a/AnkiDroid/src/main/res/values-am/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-am/17-model-manager.xml @@ -50,13 +50,13 @@ ማስታወሻ አይነቱን እንደገና ይሰይሙ Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-ar/17-model-manager.xml b/AnkiDroid/src/main/res/values-ar/17-model-manager.xml index 5c842866187f..8dc6fb55f08d 100644 --- a/AnkiDroid/src/main/res/values-ar/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ar/17-model-manager.xml @@ -58,13 +58,13 @@ تغيير اسم نوع الملحوظة تغيير اسم نوع البطاقة - تعيين تلميح اللغة إلى %s + تعيين تلميح اللغة إلى %s إضافة حقل حذف الحقل تغيير اسم الحقل تعيين تلميح لغة لوحة المفاتيح - تغيير موضع الحقل + تغيير موضع الحقل يجري تحديث الحقول فرز حسب هذا الحقل diff --git a/AnkiDroid/src/main/res/values-az/17-model-manager.xml b/AnkiDroid/src/main/res/values-az/17-model-manager.xml index c84232ff5f6a..b2ee5818b063 100644 --- a/AnkiDroid/src/main/res/values-az/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-az/17-model-manager.xml @@ -50,13 +50,13 @@ Qeyd növünü yenidən adlandır Kart növünü yenidən adlandır - Set language hint to %s + Set language hint to %s Sahə əlavə et Sahəni sil Sahəni yenidən adlandır Set keyboard language hint - Sahənin yerini dəyişdir + Sahənin yerini dəyişdir Sahələr yenilənir Bu sahəyə görə sırala diff --git a/AnkiDroid/src/main/res/values-be/17-model-manager.xml b/AnkiDroid/src/main/res/values-be/17-model-manager.xml index 778f56dbfcc8..3428bdd7a1d1 100644 --- a/AnkiDroid/src/main/res/values-be/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-be/17-model-manager.xml @@ -54,13 +54,13 @@ Перайменаваць тып нататкі Перайменаваць тып карткі - Прызначыць %s у якасці мовы падказак + Прызначыць %s у якасці мовы падказак Дадаць поле Выдаліць поле Перайменаваць поле Прызначыць мову падказак клавіятуры - Перамясціць поле + Перамясціць поле Адбываецца абнаўленне палёў Сартаваць па гэтым полі diff --git a/AnkiDroid/src/main/res/values-bg/17-model-manager.xml b/AnkiDroid/src/main/res/values-bg/17-model-manager.xml index 27b58ef620db..f1412270bed2 100644 --- a/AnkiDroid/src/main/res/values-bg/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-bg/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Добавяне на поле Изтриване на поле Преименуване на поле Set keyboard language hint - Препозициониране на поле + Препозициониране на поле Актуализиране на полетата Сортиране по това поле diff --git a/AnkiDroid/src/main/res/values-bn/17-model-manager.xml b/AnkiDroid/src/main/res/values-bn/17-model-manager.xml index ee650768f85c..7828cd10f604 100644 --- a/AnkiDroid/src/main/res/values-bn/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-bn/17-model-manager.xml @@ -50,13 +50,13 @@ নোট টাইপ নাম পরিবর্তন করুন Rename card type - ভাষার ইঙ্গিতটি %s এ সেট করুন + ভাষার ইঙ্গিতটি %s এ সেট করুন ক্ষেত্র যোগ করুন ক্ষেত্রটি মুছুন ক্ষেত্রের নাম পরিবর্তন করুন কীবোর্ড ভাষার ইঙ্গিত সেট করুন - Reposition field + Reposition field ক্ষেত্র আপডেট করা হচ্ছে এই ক্ষেত্র অনুসারে সাজান diff --git a/AnkiDroid/src/main/res/values-ca/17-model-manager.xml b/AnkiDroid/src/main/res/values-ca/17-model-manager.xml index e55d14789b22..b8ceade32a4b 100644 --- a/AnkiDroid/src/main/res/values-ca/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ca/17-model-manager.xml @@ -50,13 +50,13 @@ Canvieu el nom del tipus de nota Rename card type - Estableix consell d\'idioma a %s + Estableix consell d\'idioma a %s Afegeix el camp Suprimeix el camp Canviar el nom del camp Estableix consell d\'idioma del teclat - Camp de repós + Camp de repós Actualitzant els camps Ordena per aquest camp diff --git a/AnkiDroid/src/main/res/values-ckb/17-model-manager.xml b/AnkiDroid/src/main/res/values-ckb/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-ckb/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ckb/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-cs/17-model-manager.xml b/AnkiDroid/src/main/res/values-cs/17-model-manager.xml index 87fce4fe1029..59103392bbdd 100644 --- a/AnkiDroid/src/main/res/values-cs/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-cs/17-model-manager.xml @@ -54,13 +54,13 @@ Přejmenovat typ poznámky Přejmenovat typ karty - Jazyk nápovědy nastaven na %s + Jazyk nápovědy nastaven na %s Přidat pole Odstranit pole Přejmenovat pole Nastavit jazyk nápovědy klávesnice - Změnit umístění pole + Změnit umístění pole Aktualizace polí Seřadit podle tohoto pole diff --git a/AnkiDroid/src/main/res/values-da/17-model-manager.xml b/AnkiDroid/src/main/res/values-da/17-model-manager.xml index 2d4c63613ae2..ce0ac5a11bd9 100644 --- a/AnkiDroid/src/main/res/values-da/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-da/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-de/17-model-manager.xml b/AnkiDroid/src/main/res/values-de/17-model-manager.xml index 6d7f049efe0e..99f176d3e63e 100644 --- a/AnkiDroid/src/main/res/values-de/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-de/17-model-manager.xml @@ -50,13 +50,13 @@ Notiztyp umbenennen Kartentyp umbenennen - Sprachhinweis auf %s festlegen + Sprachhinweis auf %s festlegen Feld hinzufügen Feld löschen Feld umbenennen Hinweis zu Tastatursprache festlegen - Feld neu positionieren + Feld neu positionieren Felder werden aktualisiert Nach diesem Feld sortieren diff --git a/AnkiDroid/src/main/res/values-el/17-model-manager.xml b/AnkiDroid/src/main/res/values-el/17-model-manager.xml index 1e6e9f070fb4..be40b8254e10 100644 --- a/AnkiDroid/src/main/res/values-el/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-el/17-model-manager.xml @@ -50,13 +50,13 @@ Μετονομασία τύπου σημείωσης Μετονομασία τύπου κάρτας - Ορισμός γλώσσας υποδείξεων σε %s + Ορισμός γλώσσας υποδείξεων σε %s Προσθήκη πεδίου Διαγραφή πεδίου Μετονομασία πεδίου Ορισμός γλώσσας υπόδειξης πληκτρολογίου - Επανατοποθέτηση πεδίου + Επανατοποθέτηση πεδίου Ενημέρωση πεδίων Ταξινόμηση κατά αυτό το πεδίο diff --git a/AnkiDroid/src/main/res/values-eo/17-model-manager.xml b/AnkiDroid/src/main/res/values-eo/17-model-manager.xml index 97ab9bd1b24d..de7d9b4f1502 100644 --- a/AnkiDroid/src/main/res/values-eo/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-eo/17-model-manager.xml @@ -50,13 +50,13 @@ Alinomi nototipon Alinomi kartotipon - Agordi lingvon de sugestoj al %s + Agordi lingvon de sugestoj al %s Aldoni kampon Forigi kampon Alinomi kampon Agordi lingvon de klavaraj sugestoj - Repozicii kampon + Repozicii kampon Ĝisdatigado de kampoj Ordigi laŭ tiu kampo diff --git a/AnkiDroid/src/main/res/values-es-rAR/17-model-manager.xml b/AnkiDroid/src/main/res/values-es-rAR/17-model-manager.xml index b4bad7a06011..a8ac1a7c8ecf 100644 --- a/AnkiDroid/src/main/res/values-es-rAR/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-es-rAR/17-model-manager.xml @@ -50,13 +50,13 @@ Renombrar tipo de nota Renombrar tipo de tarjeta - Establecer pista de idioma a %s + Establecer pista de idioma a %s Añadir campo Eliminar campo Renombrar campo Establecer pista de idioma del teclado - Campo de reposicionamiento + Campo de reposicionamiento Actualizando campos Ordenar por este campo diff --git a/AnkiDroid/src/main/res/values-es-rES/17-model-manager.xml b/AnkiDroid/src/main/res/values-es-rES/17-model-manager.xml index 24eaf13e719a..d5f2f00771ef 100644 --- a/AnkiDroid/src/main/res/values-es-rES/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-es-rES/17-model-manager.xml @@ -50,13 +50,13 @@ Renombrar tipo de nota Renombrar tipo de tarjeta - Establecer pista de idioma a %s + Establecer pista de idioma a %s Añadir campo Borrar campo Renombrar campo Establecer pista de idioma del teclado - Campo de reposición + Campo de reposición Actualizando campos Ordenar por este campo diff --git a/AnkiDroid/src/main/res/values-et/17-model-manager.xml b/AnkiDroid/src/main/res/values-et/17-model-manager.xml index 84f1aaa9505d..fc1fbc5b04b9 100644 --- a/AnkiDroid/src/main/res/values-et/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-et/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Lisa väli Kustuta väli Nimeta ümber väli Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-eu/17-model-manager.xml b/AnkiDroid/src/main/res/values-eu/17-model-manager.xml index cff6e8afff9d..abb4fd63c916 100644 --- a/AnkiDroid/src/main/res/values-eu/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-eu/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Gehitu eremua Ezabatu eremua Berrizendatu eremua Set keyboard language hint - Reposition field + Reposition field Eremuak eguneratzen Sort by this field diff --git a/AnkiDroid/src/main/res/values-fa/17-model-manager.xml b/AnkiDroid/src/main/res/values-fa/17-model-manager.xml index f773f7a0d4e1..aad2a7874ec1 100644 --- a/AnkiDroid/src/main/res/values-fa/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fa/17-model-manager.xml @@ -50,13 +50,13 @@ تغییر نام نوع یادداشت Rename card type - تنظیم زبان راهنمایی به %s + تنظیم زبان راهنمایی به %s افزودن فیلد حذف فیلد تغییر نام فیلد راهنمایی زبان کیبورد را تنظیم کن - تغییر مکان فیلد + تغییر مکان فیلد به روزرسانی فیلدها مرتب کردن بر اساس این فیلد diff --git a/AnkiDroid/src/main/res/values-fi/17-model-manager.xml b/AnkiDroid/src/main/res/values-fi/17-model-manager.xml index a8d5cfad1592..d20b90f6424d 100644 --- a/AnkiDroid/src/main/res/values-fi/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fi/17-model-manager.xml @@ -50,13 +50,13 @@ Nimeä muistiinpanotyyppi uudelleen Uudelleennimeä korttityyppi - Aseta kielen vihjeeksi %s + Aseta kielen vihjeeksi %s Lisää kenttä Poista kenttä Uudelleennimeä kenttä Aseta näppäimistön kielen vihje - Uudelleensijoita kenttä + Uudelleensijoita kenttä Päivitetään kenttiä Järjestä tämän kentän mukaan diff --git a/AnkiDroid/src/main/res/values-fil/17-model-manager.xml b/AnkiDroid/src/main/res/values-fil/17-model-manager.xml index 6f7e58083c30..9c12b2d04ebd 100644 --- a/AnkiDroid/src/main/res/values-fil/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fil/17-model-manager.xml @@ -50,13 +50,13 @@ Palitan ang pangalan ng uri ng tala. Rename card type - I-set ang hint sa wika sa %s + I-set ang hint sa wika sa %s Magdagdag ng field Tanggalin ang patlang Palitan ang pangalan ng patlang I-set ang hint ng wika ng keyboard - Reposition field + Reposition field Ina-update ang mga patlang Ayusin ayon sa patlang na ito diff --git a/AnkiDroid/src/main/res/values-fr/17-model-manager.xml b/AnkiDroid/src/main/res/values-fr/17-model-manager.xml index 72286c0fee64..0f0a342f23bc 100644 --- a/AnkiDroid/src/main/res/values-fr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fr/17-model-manager.xml @@ -50,13 +50,13 @@ Renommer le type de note Renommer le type de carte - Suggestion de langue définie sur %s + Suggestion de langue définie sur %s Ajouter un champ Supprimer le champ Renommer le champ Définir la suggestion de langue du clavier - Repositionner le champ + Repositionner le champ Mise à jour des champs Trier par ce champ diff --git a/AnkiDroid/src/main/res/values-fy/17-model-manager.xml b/AnkiDroid/src/main/res/values-fy/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-fy/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fy/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-ga/17-model-manager.xml b/AnkiDroid/src/main/res/values-ga/17-model-manager.xml index af219d53f992..8fd16e1f6458 100644 --- a/AnkiDroid/src/main/res/values-ga/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ga/17-model-manager.xml @@ -56,13 +56,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-gl/17-model-manager.xml b/AnkiDroid/src/main/res/values-gl/17-model-manager.xml index 446b788dcf43..6f0b50b30073 100644 --- a/AnkiDroid/src/main/res/values-gl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-gl/17-model-manager.xml @@ -50,13 +50,13 @@ Renomear tipo de nota Renomear tipo de tarxeta - Estableceuse a pista do idioma a %s + Estableceuse a pista do idioma a %s Engadir campo Eliminar campo Renomear campo Establecer pista do idioma do teclado - Campo de reposición + Campo de reposición Actualizando campos Ordenar por este campo diff --git a/AnkiDroid/src/main/res/values-got/17-model-manager.xml b/AnkiDroid/src/main/res/values-got/17-model-manager.xml index 1fb7adda22de..b89097969d4d 100644 --- a/AnkiDroid/src/main/res/values-got/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-got/17-model-manager.xml @@ -50,13 +50,13 @@ 𐌼𐌴𐌻𐌴𐌹𐌽𐌰𐌹𐍃 𐌺𐌹𐌽𐌸 𐌰𐍆𐍄𐍂𐌰𐌷𐌰𐌹𐍄𐌰𐌽 𐌺𐌹𐌽𐌸 𐌺𐌰𐍂𐍄𐍉𐍃 𐌰𐍆𐍄𐍂𐌰𐌷𐌰𐌹𐍄𐌰𐌽 - 𐌷𐌹𐌻𐍀𐌰 𐍂𐌰𐌶𐌳𐍉𐍃 𐌻𐌰𐌲𐌾𐌰𐌽 𐌳𐌿 %s + 𐌷𐌹𐌻𐍀𐌰 𐍂𐌰𐌶𐌳𐍉𐍃 𐌻𐌰𐌲𐌾𐌰𐌽 𐌳𐌿 %s 𐍅𐌰𐌹𐍂𐌸 𐌱𐌹𐌰𐌿𐌺𐌰𐌽 𐍅𐌰𐌹𐍂𐌸𐌰 𐌿𐍃𐌵𐌹𐍃𐍄𐌾𐌰𐌽 𐍅𐌰𐌹𐍂𐌸 𐌰𐍆𐍄𐍂𐌰𐌷𐌰𐌹𐍄𐌰𐌽 𐌱𐍉𐌺𐌰𐌱𐌰𐌿𐍂𐌳𐌹𐍃 𐌷𐌹𐌻𐍀𐌰 𐍂𐌰𐌶𐌳𐍉 𐌻𐌰𐌲𐌾𐌰𐌽 - Reposition field + Reposition field 𐌰𐍆𐍄𐍂𐌰𐌰𐌽𐌰𐌽𐌹𐌿𐌾𐌰𐌽𐌳𐌰 𐍅𐌰𐌹𐍂𐌸𐌰 Sort by this field diff --git a/AnkiDroid/src/main/res/values-gu/17-model-manager.xml b/AnkiDroid/src/main/res/values-gu/17-model-manager.xml index 7371ab33b04d..c5800b5f8a6d 100644 --- a/AnkiDroid/src/main/res/values-gu/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-gu/17-model-manager.xml @@ -50,13 +50,13 @@ નોંધ પ્રકારનું નામ બદલો Rename card type - ભાષા સંકેતને %s પર સેટ કરો + ભાષા સંકેતને %s પર સેટ કરો ક્ષેત્ર ઉમેરો ક્ષેત્ર કાઢી નાખો ક્ષેત્રનું નામ બદલો કીબોર્ડ ભાષા સંકેત સેટ કરો - રિપોઝિશન ફીલ્ડ + રિપોઝિશન ફીલ્ડ ફીલ્ડ અપડેટ કરી રહ્યું છે આ ક્ષેત્ર દ્વારા સૉર્ટ કરો diff --git a/AnkiDroid/src/main/res/values-heb/17-model-manager.xml b/AnkiDroid/src/main/res/values-heb/17-model-manager.xml index 5644ef5cc92b..c3301b4102e2 100644 --- a/AnkiDroid/src/main/res/values-heb/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-heb/17-model-manager.xml @@ -54,13 +54,13 @@ שינוי שם לסוג הרשומה שינוי שם סוג הכרטיס - הגדר רמז שפה ל-%s + הגדר רמז שפה ל-%s הוספת שדה מחיקת שדה שינוי שם שדה הגדר שפת המקלדת לרמז - שינוי מיקום שדה + שינוי מיקום שדה השדות מעודכנים מיין לפי שדה diff --git a/AnkiDroid/src/main/res/values-hi/17-model-manager.xml b/AnkiDroid/src/main/res/values-hi/17-model-manager.xml index c188fb58363c..00b63de7b6e1 100644 --- a/AnkiDroid/src/main/res/values-hi/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-hi/17-model-manager.xml @@ -50,13 +50,13 @@ नोट के प्रकार का नाम बदलें Rename card type - भाषा संकेत के रूप में %s सेट करें + भाषा संकेत के रूप में %s सेट करें फ़ील्ड जोड़ें फ़ील्ड मिटाएँ फ़ील्ड का नाम बदलें कीबोर्ड भाषा संकेत सेट करें - फ़ील्ड का स्थान बदलें + फ़ील्ड का स्थान बदलें फ़ील्ड अद्यतित हो रहे हैं इस फ़ील्ड के अनुसार क्रम में रखें diff --git a/AnkiDroid/src/main/res/values-hr/17-model-manager.xml b/AnkiDroid/src/main/res/values-hr/17-model-manager.xml index 6e777eed5898..3fd1d9ff59af 100644 --- a/AnkiDroid/src/main/res/values-hr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-hr/17-model-manager.xml @@ -52,13 +52,13 @@ Preimenuj tip bilješke Preimenuj tip kartice - Postavi jezik polja na %s + Postavi jezik polja na %s Dodaj polje Izbriši polje Preimenuj polje Postavi jezik tipkovnice za polje - Premjesti polje + Premjesti polje Ažuriranje polja Sortiraj po ovom polju diff --git a/AnkiDroid/src/main/res/values-hu/17-model-manager.xml b/AnkiDroid/src/main/res/values-hu/17-model-manager.xml index c3200816b5fe..8b5b6e23ee6d 100644 --- a/AnkiDroid/src/main/res/values-hu/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-hu/17-model-manager.xml @@ -50,13 +50,13 @@ Jegyzettípus átnevezése Rename card type - Nyelvi javaslat beállítás %s + Nyelvi javaslat beállítás %s Mező hozzáadása Mező törlése Mező átnevezése Billentyűzet nyelvi javaslat beállítás - Áthelyezési mező + Áthelyezési mező Mezők frissítése Aktuális mező szerinti rendezés diff --git a/AnkiDroid/src/main/res/values-hy/17-model-manager.xml b/AnkiDroid/src/main/res/values-hy/17-model-manager.xml index bef18e9705e2..3ed7656621af 100644 --- a/AnkiDroid/src/main/res/values-hy/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-hy/17-model-manager.xml @@ -50,13 +50,13 @@ Վերանվանել տեսակը Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-ind/17-model-manager.xml b/AnkiDroid/src/main/res/values-ind/17-model-manager.xml index 5f6b66c07bff..05174d5044fb 100644 --- a/AnkiDroid/src/main/res/values-ind/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ind/17-model-manager.xml @@ -48,13 +48,13 @@ Ubah nama tipe catatan Rename card type - Atur petunjuk bahasa menjadi %s + Atur petunjuk bahasa menjadi %s Tambah kolom Hapus kolom Ubah nama kolom Atur petunjuk bahasa papan tik - Pindah posisi bidang + Pindah posisi bidang Memperbarui kolom Urutkan menurut kolom ini diff --git a/AnkiDroid/src/main/res/values-it/17-model-manager.xml b/AnkiDroid/src/main/res/values-it/17-model-manager.xml index 34857f21fd81..981d7e2c4356 100644 --- a/AnkiDroid/src/main/res/values-it/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-it/17-model-manager.xml @@ -50,13 +50,13 @@ Rinoma il tipo di nota Rinomina il tipo di carta - Imposta suggerimento lingua a %s + Imposta suggerimento lingua a %s Aggiungi campo Elimina campo Rinomina il campo Imposta suggerimento lingua tastiera - Riposiziona il campo + Riposiziona il campo Aggiornamento dei campi Ordina per questo campo diff --git a/AnkiDroid/src/main/res/values-iw/17-model-manager.xml b/AnkiDroid/src/main/res/values-iw/17-model-manager.xml index 5644ef5cc92b..c3301b4102e2 100644 --- a/AnkiDroid/src/main/res/values-iw/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-iw/17-model-manager.xml @@ -54,13 +54,13 @@ שינוי שם לסוג הרשומה שינוי שם סוג הכרטיס - הגדר רמז שפה ל-%s + הגדר רמז שפה ל-%s הוספת שדה מחיקת שדה שינוי שם שדה הגדר שפת המקלדת לרמז - שינוי מיקום שדה + שינוי מיקום שדה השדות מעודכנים מיין לפי שדה diff --git a/AnkiDroid/src/main/res/values-ja/17-model-manager.xml b/AnkiDroid/src/main/res/values-ja/17-model-manager.xml index 3d1c76687081..4679d2140c8a 100644 --- a/AnkiDroid/src/main/res/values-ja/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ja/17-model-manager.xml @@ -48,13 +48,13 @@ ノートタイプの名前を変更 カードタイプの名前を変更 - %s をキーボード言語に設定しました + %s をキーボード言語に設定しました フィールドを追加 削除 名前を変更 キーボード言語を設定 - 配置順序を変更 + 配置順序を変更 フィールドを更新 ブラウザでのソートフィールド(見出し・並べ替え用項目)に指定 diff --git a/AnkiDroid/src/main/res/values-ka/17-model-manager.xml b/AnkiDroid/src/main/res/values-ka/17-model-manager.xml index b68c6d1631d3..2eccd89316f1 100644 --- a/AnkiDroid/src/main/res/values-ka/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ka/17-model-manager.xml @@ -50,13 +50,13 @@ შენიშვნის ტიპის სახელის შეცვლა Rename card type - %s-თან ენის მინიშნების დაყენება + %s-თან ენის მინიშნების დაყენება ველის დამატება ველის წაშლა ველის სახელის შეცვლა კლავიატურის ენის მინიშნების დაყენება - ველის გადაადგილება + ველის გადაადგილება ველების განახლება ამ ველის მიხედვით დახარისხება diff --git a/AnkiDroid/src/main/res/values-kk/17-model-manager.xml b/AnkiDroid/src/main/res/values-kk/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-kk/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-kk/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-km/17-model-manager.xml b/AnkiDroid/src/main/res/values-km/17-model-manager.xml index a8d20a273775..c3968893d949 100644 --- a/AnkiDroid/src/main/res/values-km/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-km/17-model-manager.xml @@ -48,13 +48,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-kn/17-model-manager.xml b/AnkiDroid/src/main/res/values-kn/17-model-manager.xml index 6f97f96fb3f0..93d1f62e1fe4 100644 --- a/AnkiDroid/src/main/res/values-kn/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-kn/17-model-manager.xml @@ -50,13 +50,13 @@ ಟಿಪ್ಪಣಿ ಪ್ರಕಾರವನ್ನು ಮರುಹೆಸರಿಸಿ Rename card type - ಭಾಷೆಯ ಸುಳಿವನ್ನು %s ಗೆ ಹೊಂದಿಸಿ + ಭಾಷೆಯ ಸುಳಿವನ್ನು %s ಗೆ ಹೊಂದಿಸಿ ಕ್ಷೇತ್ರವನ್ನು ಸೇರಿಸಿ ಕ್ಷೇತ್ರವನ್ನು ಅಳಿಸಿ ಕ್ಷೇತ್ರವನ್ನು ಮರುಹೆಸರಿಸಿ ಕೀಬೋರ್ಡ್ ಭಾಷೆಯ ಸುಳಿವನ್ನು ಹೊಂದಿಸಿ - ಮರುಸ್ಥಾಪನೆ ಕ್ಷೇತ್ರ + ಮರುಸ್ಥಾಪನೆ ಕ್ಷೇತ್ರ ಕ್ಷೇತ್ರಗಳನ್ನು ನವೀಕರಿಸಲಾಗುತ್ತಿದೆ ಈ ಕ್ಷೇತ್ರದಿಂದ ವಿಂಗಡಿಸಿ diff --git a/AnkiDroid/src/main/res/values-ko/17-model-manager.xml b/AnkiDroid/src/main/res/values-ko/17-model-manager.xml index 81e56f249aa9..80b696941c0b 100644 --- a/AnkiDroid/src/main/res/values-ko/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ko/17-model-manager.xml @@ -48,13 +48,13 @@ 노트 유형 이름 바꾸기 카드 유형 이름 바꾸기 - 키보드 언어 힌트를 %s로 설정하기 + 키보드 언어 힌트를 %s로 설정하기 필드 추가하기 필드 삭제하기 필드 이름 변경하기 키보드 언어 힌트 설정하기 - 필드 재배치하기 + 필드 재배치하기 필드 업데이트 중 이 필드로 정렬하기 diff --git a/AnkiDroid/src/main/res/values-ku/17-model-manager.xml b/AnkiDroid/src/main/res/values-ku/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-ku/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ku/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-ky/17-model-manager.xml b/AnkiDroid/src/main/res/values-ky/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-ky/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ky/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-lt/17-model-manager.xml b/AnkiDroid/src/main/res/values-lt/17-model-manager.xml index 9b91878a258d..a45c1891cdc6 100644 --- a/AnkiDroid/src/main/res/values-lt/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-lt/17-model-manager.xml @@ -54,13 +54,13 @@ Pervadinti užrašo tipą Rename card type - Nustatyti kalbos užuominą į %s + Nustatyti kalbos užuominą į %s Pridėti laukelį Ištrinti laukelį Pervardinti laukelį Nustatyti klaviatūros kalbos užuominą - Keisti laukelio vietą + Keisti laukelio vietą Atnaujinami laukeliai Rūšiuoti pagal šį lauką diff --git a/AnkiDroid/src/main/res/values-lv/17-model-manager.xml b/AnkiDroid/src/main/res/values-lv/17-model-manager.xml index bb09a78c6d35..320831d8cec1 100644 --- a/AnkiDroid/src/main/res/values-lv/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-lv/17-model-manager.xml @@ -52,13 +52,13 @@ Pārdēvēt piezīmju veidu Rename card type - Iestatīt valodas norādi %s + Iestatīt valodas norādi %s Pievienot lauku Izdzēst lauku Pārdēvēt lauku Iestatīt tastatūras valodas norādi - Pārkārtot lauku + Pārkārtot lauku Atjaunina laukus Kārtot pēc šī lauka diff --git a/AnkiDroid/src/main/res/values-mk/17-model-manager.xml b/AnkiDroid/src/main/res/values-mk/17-model-manager.xml index 1084744b57e5..42d8c8db1b64 100644 --- a/AnkiDroid/src/main/res/values-mk/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-mk/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Додај поле Избриши поле Преименувај го полето Set keyboard language hint - Поле за поместување + Поле за поместување Ажурирање на полињата Подреди според ова поле diff --git a/AnkiDroid/src/main/res/values-ml/17-model-manager.xml b/AnkiDroid/src/main/res/values-ml/17-model-manager.xml index 57455081ca8d..53dd1f095b47 100644 --- a/AnkiDroid/src/main/res/values-ml/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ml/17-model-manager.xml @@ -50,13 +50,13 @@ നോട്ട് തരം പുനർനാമകരണം ചെയ്യുക Rename card type - ഭാഷാ സൂചന %s ആയി സജ്ജമാക്കുക + ഭാഷാ സൂചന %s ആയി സജ്ജമാക്കുക ഫീൽഡ് ചേർക്കുക ഫീൽഡ് ഇല്ലാതാക്കുക ഫീൽഡിൻ്റെ പേര് മാറ്റുക കീബോർഡ് ഭാഷാ സൂചന സജ്ജമാക്കുക - Reposition field + Reposition field ഫീൽഡുകൾ അപ്ഡേറ്റ് ചെയ്യുന്നു ഈ ഫീൽഡ് അനുസരിച്ച് അടുക്കുക diff --git a/AnkiDroid/src/main/res/values-mn/17-model-manager.xml b/AnkiDroid/src/main/res/values-mn/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-mn/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-mn/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-mr/17-model-manager.xml b/AnkiDroid/src/main/res/values-mr/17-model-manager.xml index bd9fc7f257f6..9ba62c48456a 100644 --- a/AnkiDroid/src/main/res/values-mr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-mr/17-model-manager.xml @@ -50,13 +50,13 @@ टीप प्रकार पुनर्नामित करा Rename card type - भाषेचा संकेत%s वर सेट करा + भाषेचा संकेत%s वर सेट करा फील्ड जोडा फील्ड हटवा फील्ड पुनर्नामित करा कीबोर्ड भाषेचा संकेत सेट करा - स्थान फील्ड + स्थान फील्ड फील्ड अद्यतनित करीत आहे या फील्डनुसार क्रमवारी लावा diff --git a/AnkiDroid/src/main/res/values-ms/17-model-manager.xml b/AnkiDroid/src/main/res/values-ms/17-model-manager.xml index 2bb8d267618b..06091b4add19 100644 --- a/AnkiDroid/src/main/res/values-ms/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ms/17-model-manager.xml @@ -48,13 +48,13 @@ Nama semula jenis nota Rename card type - Tetapkan petunjuk bahasa kepada %s + Tetapkan petunjuk bahasa kepada %s Tambah Medan Hapuskan medan Nama semula medan Tetapkan petunjuk bahasa papan kekunci - Alih posisi medan + Alih posisi medan Mengemas kini medan Susun ikut medan ini diff --git a/AnkiDroid/src/main/res/values-my/17-model-manager.xml b/AnkiDroid/src/main/res/values-my/17-model-manager.xml index a8d20a273775..c3968893d949 100644 --- a/AnkiDroid/src/main/res/values-my/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-my/17-model-manager.xml @@ -48,13 +48,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-nl/17-model-manager.xml b/AnkiDroid/src/main/res/values-nl/17-model-manager.xml index dffd51084132..239b6a5d2d9d 100644 --- a/AnkiDroid/src/main/res/values-nl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-nl/17-model-manager.xml @@ -50,13 +50,13 @@ Memotype hernoemen Rename card type - Taaltip op %s instellen + Taaltip op %s instellen Veld toevoegen Veld verwijderen Naam van veld wijzigen Suggestie voor de toetsenbordtaal instellen - Verplaats veld + Verplaats veld Velden bijwerken Sorteren op dit veld diff --git a/AnkiDroid/src/main/res/values-nn/17-model-manager.xml b/AnkiDroid/src/main/res/values-nn/17-model-manager.xml index 6d65afd4d4d8..40767483afe0 100644 --- a/AnkiDroid/src/main/res/values-nn/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-nn/17-model-manager.xml @@ -50,13 +50,13 @@ Gi nytt navn til notattype Rename card type - Set language hint to %s + Set language hint to %s Legg til felt Slett felt Gjev felt nytt namn Set keyboard language hint - Flytt felt + Flytt felt Oppdaterer felt Sorter etter dette feltet diff --git a/AnkiDroid/src/main/res/values-no/17-model-manager.xml b/AnkiDroid/src/main/res/values-no/17-model-manager.xml index 3b0ea3fc6e60..74c0d3823cb0 100644 --- a/AnkiDroid/src/main/res/values-no/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-no/17-model-manager.xml @@ -50,13 +50,13 @@ Gi nytt navn til notattype Rename card type - Set language hint to %s + Set language hint to %s Legg til felt Slett felt Endre navn på felt Set keyboard language hint - Flytt felt + Flytt felt Oppdaterer felt Sorter etter dette feltet diff --git a/AnkiDroid/src/main/res/values-or/17-model-manager.xml b/AnkiDroid/src/main/res/values-or/17-model-manager.xml index 5f6be8c42650..652a09cca9c3 100644 --- a/AnkiDroid/src/main/res/values-or/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-or/17-model-manager.xml @@ -50,13 +50,13 @@ ନୋଟ୍ ପ୍ରକାରର ନାମ ବଦଳାଇବା ପତ୍ର ପ୍ରକାରର ନାମ ବଦଳାଇବା - ଭାଷା ସୂଚକ କୁ %s ସେଟ୍ କରନ୍ତୁ + ଭାଷା ସୂଚକ କୁ %s ସେଟ୍ କରନ୍ତୁ କ୍ଷେତ୍ର ଯୋଡ଼ିବା କ୍ଷେତ୍ରଟି ଵିଲୋପ କରିବା କ୍ଷେତ୍ରଟିର ନାମ ବଦଳାଇବା କୀବୋର୍ଡର ସୂଚକ ଭାଷା ସେଟ୍ କରନ୍ତୁ - କ୍ଷେତ୍ରଟିକୁ ପୁନଃଅଵସ୍ଥିତ କରିବା + କ୍ଷେତ୍ରଟିକୁ ପୁନଃଅଵସ୍ଥିତ କରିବା କ୍ଷେତ୍ରଗୁଡ଼ିକ ଅଦ୍ୟତିତ ହେଉଛି ଏହି କ୍ଷେତ୍ର ଅନୁଯାୟୀ ସଜାନ୍ତୁ diff --git a/AnkiDroid/src/main/res/values-pa/17-model-manager.xml b/AnkiDroid/src/main/res/values-pa/17-model-manager.xml index 0a78b6d8725d..36fd30478efa 100644 --- a/AnkiDroid/src/main/res/values-pa/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-pa/17-model-manager.xml @@ -50,13 +50,13 @@ ਨੋਟ ਦੀ ਕਿਸਮ ਦਾ ਨਾਮ ਬਦਲੋ Rename card type - ਭਾਸ਼ਾ ਸੰਕੇਤਕ ਨੂੰ %s \'ਤੇ ਸੈੱਟ ਕਰੋ + ਭਾਸ਼ਾ ਸੰਕੇਤਕ ਨੂੰ %s \'ਤੇ ਸੈੱਟ ਕਰੋ ਖੇਤਰ ਸ਼ਾਮਲ ਕਰੋ ਖੇਤਰ ਮਿਟਾਓ ਖੇਤਰ ਦਾ ਨਾਮ ਬਦਲੋ ਕੀਬੋਰਡ ਭਾਸ਼ਾ ਸੰਕੇਤ ਸੈੱਟ ਕਰੋ - ਰੀਪੋਜੀਸ਼ਨ ਫੀਲਡ + ਰੀਪੋਜੀਸ਼ਨ ਫੀਲਡ ਖੇਤਰਾਂ ਨੂੰ ਅੱਪਡੇਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ ਇਸ ਖੇਤਰ ਦੁਆਰਾ ਕ੍ਰਮਬੱਧ ਕਰੋ diff --git a/AnkiDroid/src/main/res/values-pl/17-model-manager.xml b/AnkiDroid/src/main/res/values-pl/17-model-manager.xml index c46a53a62e85..84062c23a54e 100644 --- a/AnkiDroid/src/main/res/values-pl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-pl/17-model-manager.xml @@ -54,13 +54,13 @@ Zmień nazwę typu notatki Zmień nazwę typu karty - Ustaw podpowiedź językową na %s + Ustaw podpowiedź językową na %s Dodaj pole Usuń pole Zmień nazwę pola Ustaw podpowiedź języka klawiatury - Zmień pozycję pola + Zmień pozycję pola Aktualizowanie pól Sortuj według tego pola diff --git a/AnkiDroid/src/main/res/values-pt-rBR/17-model-manager.xml b/AnkiDroid/src/main/res/values-pt-rBR/17-model-manager.xml index b216305874e2..e88e6fbcf0cb 100644 --- a/AnkiDroid/src/main/res/values-pt-rBR/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-pt-rBR/17-model-manager.xml @@ -50,13 +50,13 @@ Renomear categorias de nota Nomear tipo de carta - Definir idioma da dica para %s + Definir idioma da dica para %s Adicionar campo Excluir campo Renomear campo Definir idioma do teclado - Campo de reposição + Campo de reposição Atualizando os campos Ordenar por esse campo diff --git a/AnkiDroid/src/main/res/values-pt-rPT/17-model-manager.xml b/AnkiDroid/src/main/res/values-pt-rPT/17-model-manager.xml index ccf5a99511e6..17c09263965d 100644 --- a/AnkiDroid/src/main/res/values-pt-rPT/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-pt-rPT/17-model-manager.xml @@ -50,13 +50,13 @@ Renomear tipo de nota Renomeie o tipo de ficha - Definir a língua associada como %s + Definir a língua associada como %s Adicionar campo Eliminar campo Renomear campo Definir a língua associada ao teclado - Campo de reposição + Campo de reposição A atualizar os campos Ordenar por este campo diff --git a/AnkiDroid/src/main/res/values-ro/17-model-manager.xml b/AnkiDroid/src/main/res/values-ro/17-model-manager.xml index 174016886302..8bb865abe8ad 100644 --- a/AnkiDroid/src/main/res/values-ro/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ro/17-model-manager.xml @@ -52,13 +52,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Actualizare câmpuri Sortează după acest câmp diff --git a/AnkiDroid/src/main/res/values-ru/17-model-manager.xml b/AnkiDroid/src/main/res/values-ru/17-model-manager.xml index a817379df65d..d26d677fa90a 100644 --- a/AnkiDroid/src/main/res/values-ru/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ru/17-model-manager.xml @@ -54,13 +54,13 @@ Переименовать тип записи Переименовать тип карточки - Установить %s как язык подсказок + Установить %s как язык подсказок Добавить поле Удалить поле Переименовать поле Задать язык подсказок клавиатуры - Переместить поле + Переместить поле Поля обновляются Сортировать по этому полю diff --git a/AnkiDroid/src/main/res/values-sat/17-model-manager.xml b/AnkiDroid/src/main/res/values-sat/17-model-manager.xml index b9bc9ecc809f..06a64a06da6c 100644 --- a/AnkiDroid/src/main/res/values-sat/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sat/17-model-manager.xml @@ -50,13 +50,13 @@ ᱠᱷᱟᱴᱚ ᱵᱤᱪᱟᱹᱨ ᱯᱨᱚᱠᱟᱨ ᱫᱩᱦᱰᱟ ᱧᱩᱛᱩᱢ Rename card type - %s ᱛᱮ ᱯᱟᱹᱨᱥᱤ ᱦᱤᱸᱴ ᱥᱮᱴ ᱢᱮ + %s ᱛᱮ ᱯᱟᱹᱨᱥᱤ ᱦᱤᱸᱴ ᱥᱮᱴ ᱢᱮ ᱡᱟᱭᱜᱟ ᱥᱮᱞᱮᱫ ᱢᱮ ᱡᱟᱭᱜᱟ ᱜᱮᱫ ᱜᱤᱰᱤ ᱡᱟᱭᱜᱟ ᱫᱩᱦᱲᱟ ᱧᱩᱛᱩᱢᱟᱱ ᱠᱤᱵᱚᱰ ᱯᱟᱹᱨᱥᱤ ᱦᱤᱸᱴ ᱥᱮᱴ ᱢᱮ - ᱡᱟᱭᱜᱟ ᱵᱚᱫᱚᱞ + ᱡᱟᱭᱜᱟ ᱵᱚᱫᱚᱞ ᱡᱟᱭᱜᱟ ᱟᱹᱯᱰᱟᱴᱼᱚᱜ ᱠᱟᱱᱟ ᱡᱟᱭᱜᱟ ᱛᱮ ᱥᱚᱴᱼᱚᱜ ᱠᱟᱱᱟ diff --git a/AnkiDroid/src/main/res/values-sc/17-model-manager.xml b/AnkiDroid/src/main/res/values-sc/17-model-manager.xml index a2b1c0f51d7e..f71c3eefcf32 100644 --- a/AnkiDroid/src/main/res/values-sc/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sc/17-model-manager.xml @@ -50,13 +50,13 @@ Càmbia nùmene a sa casta de nota Rename card type - Imposta s\'impòsitu de limba a %s + Imposta s\'impòsitu de limba a %s Annanghe unu campu Iscantzella su campu Muda su campu de nùmene Imposta s\'impòsitu de limba de su tecladu - Torra a positzionare su campu + Torra a positzionare su campu Atualizatzione de sos campos Òrdina sighende custu campu diff --git a/AnkiDroid/src/main/res/values-sk/17-model-manager.xml b/AnkiDroid/src/main/res/values-sk/17-model-manager.xml index e236aa481f10..441ffa725450 100644 --- a/AnkiDroid/src/main/res/values-sk/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sk/17-model-manager.xml @@ -54,13 +54,13 @@ Premenovať typ poznámok Rename card type - Nastaviť jazykový tip na %s + Nastaviť jazykový tip na %s Pridať pole Vymazať pole Premenovať pole Nastaviť jazykový tip klávesnice - Premiestniť pole + Premiestniť pole Aktualizácia polí Usporiadať podľa tohto poľa diff --git a/AnkiDroid/src/main/res/values-sl/17-model-manager.xml b/AnkiDroid/src/main/res/values-sl/17-model-manager.xml index dd8e180b3422..bc428fdb8993 100644 --- a/AnkiDroid/src/main/res/values-sl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sl/17-model-manager.xml @@ -54,13 +54,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Dodaj polje Izbriši polje Preimenuj polje Set keyboard language hint - Prestavi polje + Prestavi polje Posodabljanje polj Razvrsti po temu polju diff --git a/AnkiDroid/src/main/res/values-sq/17-model-manager.xml b/AnkiDroid/src/main/res/values-sq/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-sq/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sq/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-sr/17-model-manager.xml b/AnkiDroid/src/main/res/values-sr/17-model-manager.xml index 84b0523b2e3c..77a6c9249693 100644 --- a/AnkiDroid/src/main/res/values-sr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sr/17-model-manager.xml @@ -52,13 +52,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Додај поље Избриши поље Преименуј поље Set keyboard language hint - Репозиција поље + Репозиција поље Поље ажурирања Сортирај по овом пољу diff --git a/AnkiDroid/src/main/res/values-sv/17-model-manager.xml b/AnkiDroid/src/main/res/values-sv/17-model-manager.xml index ac1282e8960b..d4cef06ce957 100644 --- a/AnkiDroid/src/main/res/values-sv/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sv/17-model-manager.xml @@ -50,13 +50,13 @@ Byt namn på nottyp Rename card type - Ställ in språktips till %s + Ställ in språktips till %s Lägg till fält Ta bort fält Byt namn på fält Ange tips på tangentbordsspråk - Flytta fält + Flytta fält Uppdaterar fält Sortera efter detta fält diff --git a/AnkiDroid/src/main/res/values-ta/17-model-manager.xml b/AnkiDroid/src/main/res/values-ta/17-model-manager.xml index b57f60b45fcf..c92d3f36b179 100644 --- a/AnkiDroid/src/main/res/values-ta/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ta/17-model-manager.xml @@ -50,13 +50,13 @@ குறிப்பு வகையை மறுபெயரிடவும் Rename card type - மொழி குறிப்பை %s ஆக அமைக்கவும் + மொழி குறிப்பை %s ஆக அமைக்கவும் ஒரு புலத்தைச் சேர்க்கவும் புலத்தை நீக்கு புலத்தை மறுபெயரிடவும் விசைப்பலகை மொழி குறிப்பை அமைக்கவும் - இடமாற்ற புலம் + இடமாற்ற புலம் புதுப்பிப்பு புலங்கள் இந்தப் புலத்தின்படி வரிசைப்படுத்தவும் diff --git a/AnkiDroid/src/main/res/values-te/17-model-manager.xml b/AnkiDroid/src/main/res/values-te/17-model-manager.xml index 1649cdc8e6c6..7755c49de6c1 100644 --- a/AnkiDroid/src/main/res/values-te/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-te/17-model-manager.xml @@ -50,13 +50,13 @@ గమనిక రకం పేరు మార్చండి Rename card type - భాష సూచనను %sకి సెట్ చేయండి + భాష సూచనను %sకి సెట్ చేయండి ఫీల్డ్ను జోడించండి ఫీల్డ్ను తొలగించు ఫీల్డ్ పేరు మార్చండి కీబోర్డ్ భాష సూచనను సెట్ చేయండి - రీపోషన్ ఫీల్డ్ + రీపోషన్ ఫీల్డ్ ఖాళీలను నవీకరిస్తోంది ఈ ఫీల్డ్ ద్వారా క్రమబద్ధీకరించు diff --git a/AnkiDroid/src/main/res/values-tgl/17-model-manager.xml b/AnkiDroid/src/main/res/values-tgl/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-tgl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-tgl/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-th/17-model-manager.xml b/AnkiDroid/src/main/res/values-th/17-model-manager.xml index 40215e48577a..5e61f43b3387 100644 --- a/AnkiDroid/src/main/res/values-th/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-th/17-model-manager.xml @@ -48,13 +48,13 @@ เปลี่ยนชื่อรูปแบบโน๊ต Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-ti/17-model-manager.xml b/AnkiDroid/src/main/res/values-ti/17-model-manager.xml index 3700e8fc92ce..c3c60b3f6ee8 100644 --- a/AnkiDroid/src/main/res/values-ti/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ti/17-model-manager.xml @@ -50,13 +50,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-tr/17-model-manager.xml b/AnkiDroid/src/main/res/values-tr/17-model-manager.xml index 2dd4cd96539a..a6a8b09c4dd3 100644 --- a/AnkiDroid/src/main/res/values-tr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-tr/17-model-manager.xml @@ -50,13 +50,13 @@ Not türünün adını değiştir Kart türünün ismini değiştir - Dil ipucunu %s olarak ayarla + Dil ipucunu %s olarak ayarla Alan ekle Alanı sil Alanın ismini değiştir Klavye dili ipucunu ayarla - Alanın yerini değiştir + Alanın yerini değiştir Alanlar güncelleniyor Bu alana göre sırala diff --git a/AnkiDroid/src/main/res/values-tt/17-model-manager.xml b/AnkiDroid/src/main/res/values-tt/17-model-manager.xml index a8d20a273775..c3968893d949 100644 --- a/AnkiDroid/src/main/res/values-tt/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-tt/17-model-manager.xml @@ -48,13 +48,13 @@ Rename note type Rename card type - Set language hint to %s + Set language hint to %s Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field diff --git a/AnkiDroid/src/main/res/values-ug/17-model-manager.xml b/AnkiDroid/src/main/res/values-ug/17-model-manager.xml index 8a6611d1933c..7630bb243469 100644 --- a/AnkiDroid/src/main/res/values-ug/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ug/17-model-manager.xml @@ -50,13 +50,13 @@ خاتىرە تۈرىنىڭ ئاتىنى ئۆزگەرتىدۇ كارتا تۈرىنىڭ ئاتىنى ئۆزگەرتىدۇ - تىل كۆرسەتمىسىنى %s غا تەڭشەيدۇ + تىل كۆرسەتمىسىنى %s غا تەڭشەيدۇ بۆلەك قوش بۆلەك ئۆچۈر بۆلەك ئاتىنى ئۆزگەرت ھەرپتاختا تىل كۆرسەتمىسىنى تەڭشەيدۇ - بۆلەك ئورنىنى تەڭشەيدۇ + بۆلەك ئورنىنى تەڭشەيدۇ بۆلەك يېڭىلا بۇ بۆلەك بويىچە تەرتىپلە diff --git a/AnkiDroid/src/main/res/values-uk/17-model-manager.xml b/AnkiDroid/src/main/res/values-uk/17-model-manager.xml index 86ae53cb27f5..b0f0e0bbe59c 100644 --- a/AnkiDroid/src/main/res/values-uk/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-uk/17-model-manager.xml @@ -54,13 +54,13 @@ Перейменувати тип запису Перейменувати тип картки - Встановити %s як мову підказок + Встановити %s як мову підказок Додати поле Видалити поле Перейменувати поле Встановлення мови підказок клавіатури - Перемістити поле + Перемістити поле Оновлення полів Сортувати за цим полем diff --git a/AnkiDroid/src/main/res/values-ur/17-model-manager.xml b/AnkiDroid/src/main/res/values-ur/17-model-manager.xml index 4ba3f0d9aaea..fd9f53dcc228 100644 --- a/AnkiDroid/src/main/res/values-ur/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ur/17-model-manager.xml @@ -50,13 +50,13 @@ نوٹ کی قسم کا نام تبدیل کریں۔ Rename card type - زبان کے اشارے کو %s پر سیٹ کریں۔ + زبان کے اشارے کو %s پر سیٹ کریں۔ فیلڈ شامل کریں فیلڈ کو حذف کریں۔ فیلڈ کا نام تبدیل کریں۔ کی بورڈ کی زبان کا اشارہ سیٹ کریں - ریپوزیشن فیلڈ + ریپوزیشن فیلڈ فیلڈز کو اپ ڈیٹ کرنا اس فیلڈ کے مطابق ترتیب دیں۔ diff --git a/AnkiDroid/src/main/res/values-uz/17-model-manager.xml b/AnkiDroid/src/main/res/values-uz/17-model-manager.xml index 8d5fad684854..7191d24d8d46 100644 --- a/AnkiDroid/src/main/res/values-uz/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-uz/17-model-manager.xml @@ -50,13 +50,13 @@ Qayd turi nomini oʻzgartirish Karta turi nomini oʻzgartirish - Til tavsiyasi %sga sozlandi + Til tavsiyasi %sga sozlandi Maydon qoʻshish Maydonni oʻchirib tashlash Maydon nomini oʻzgartirish Klaviatura tili tavsiyasini sozlash - Maydon oʻrnini oʻzgartirish + Maydon oʻrnini oʻzgartirish Maydonlar yangilanmoqda Bu maydon boʻyicha saralash diff --git a/AnkiDroid/src/main/res/values-vi/17-model-manager.xml b/AnkiDroid/src/main/res/values-vi/17-model-manager.xml index e195efaa0558..91deb7bf19c5 100644 --- a/AnkiDroid/src/main/res/values-vi/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-vi/17-model-manager.xml @@ -48,13 +48,13 @@ Đổi tên loại ghi chú Đổi tên loại thẻ - Đặt gợi ý ngôn ngữ thành %s + Đặt gợi ý ngôn ngữ thành %s Thêm trường Xóa trường Đổi tên trường Đặt gợi ý ngôn ngữ bàn phím - Định vị lại trường + Định vị lại trường Cập nhật các trường Sắp xếp theo trường này diff --git a/AnkiDroid/src/main/res/values-zh-rCN/17-model-manager.xml b/AnkiDroid/src/main/res/values-zh-rCN/17-model-manager.xml index e27240e6c7a3..be0b3def5933 100644 --- a/AnkiDroid/src/main/res/values-zh-rCN/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-zh-rCN/17-model-manager.xml @@ -48,13 +48,13 @@ 重命名笔记模板 重命名卡牌类型 - 设置语言提示为 %s + 设置语言提示为 %s 添加字段 删除字段 重命名字段 设置键盘语言提示 - 修改字段位置 + 修改字段位置 更新字段 按此字段排序 diff --git a/AnkiDroid/src/main/res/values-zh-rTW/17-model-manager.xml b/AnkiDroid/src/main/res/values-zh-rTW/17-model-manager.xml index e1e8d5990ce7..8d866583292f 100644 --- a/AnkiDroid/src/main/res/values-zh-rTW/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-zh-rTW/17-model-manager.xml @@ -48,13 +48,13 @@ 重新命名筆記類型 重命名卡片種類 - 將語言提示設為%s + 將語言提示設為%s 新增欄位 刪除欄位 重新命名欄位 選擇鍵盤提示語言 - 修改字段位置 + 修改字段位置 更新欄位 按此字段排序 diff --git a/AnkiDroid/src/main/res/values/17-model-manager.xml b/AnkiDroid/src/main/res/values/17-model-manager.xml index 8bb09f8b83c1..7447637b01f8 100644 --- a/AnkiDroid/src/main/res/values/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values/17-model-manager.xml @@ -37,19 +37,25 @@ Rename card type - Set language hint to %s - Cleared language hint - + Set language hint for ‘%1$s’ to ‘%2$s’ + Cleared language hint for ‘%1$s’ Add field Delete field Rename field Set keyboard language hint - Reposition field + Reposition field Updating fields Sort by this field - Note - Changes on this screen can be undone via the deck-picker menu. Note that language settings cannot be undone due to limitations in Anki and AnkiDroid\'s backend integration. + Added ‘%1$s’ + Deleted ‘%1$s’ + Renamed ‘%1$s’ to ‘%2$s’ + Moved ‘%1$s’ from %2$d to %3$d + Set ‘%1$s’ as the sort field + Tap the small check icon to confirm field name + Saved changes successfully + Discarded changes + These changes cannot be undone. Are you sure you wish to save these changes? diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt index 45651cecedd5..15537a864919 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt @@ -21,9 +21,11 @@ import android.content.Intent import android.view.ContextThemeWrapper import android.widget.EditText import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import com.ichi2.anki.libanki.exception.ConfirmModSchemaException import com.ichi2.utils.positiveButton import com.ichi2.utils.show +import kotlinx.coroutines.launch import org.hamcrest.MatcherAssert import org.hamcrest.Matchers import org.junit.Test @@ -125,8 +127,11 @@ class NoteTypeFieldEditorTest( intent, ) when (fieldOperationType) { - FieldOperationType.ADD_FIELD -> noteTypeFieldEditor.addField(fieldName) - FieldOperationType.RENAME_FIELD -> noteTypeFieldEditor.renameField(position, fieldName) + FieldOperationType.ADD_FIELD -> noteTypeFieldEditor.viewModel.add(name = fieldName) + FieldOperationType.RENAME_FIELD -> noteTypeFieldEditor.viewModel.rename(position, fieldName) + } + noteTypeFieldEditor.lifecycleScope.launch { + noteTypeFieldEditor.viewModel.save() } } catch (exception: ConfirmModSchemaException) { throw RuntimeException(exception) From 3dbed46f013732f82f57b0cf6c81b6c2933f9b61 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:04:49 +0900 Subject: [PATCH 10/19] fix(notetypefieldeditor): fix Formatting strings --- AnkiDroid/src/main/res/values/17-model-manager.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/res/values/17-model-manager.xml b/AnkiDroid/src/main/res/values/17-model-manager.xml index 7447637b01f8..0d8c8bfaca92 100644 --- a/AnkiDroid/src/main/res/values/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values/17-model-manager.xml @@ -37,7 +37,7 @@ Rename card type - Set language hint for ‘%1$s’ to ‘%2$s’ + Set language hint for ‘%2$s’ to ‘%1$s’ Cleared language hint for ‘%1$s’ Add field From b6f46400a00b2c7a9a40f478a8e5b3879f65926e Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:14:11 +0900 Subject: [PATCH 11/19] fix(notetypefieldeditor): fix weired recyclerview behavior and optimize operations Fixed too fast drag speed when moving editing field. Fixed uncorrect rename operation. Use LinerLayout insted of ConstraintLayout to scroll smoothly. Did away with holding unsaved operation when Activity is recreated, avoiding operation stack inconsistency. --- .../fieldeditor/NoteTypeFieldEditor.kt | 382 +++++++++++------- .../NoteTypeFieldEditorViewModel.kt | 267 ++++++------ .../fieldeditor/NoteTypeFieldOperation.kt | 18 +- .../fieldeditor/NoteTypeFieldRowData.kt | 5 +- .../main/res/layout/item_notetype_field.xml | 33 +- .../com/ichi2/anki/NoteTypeFieldEditorTest.kt | 7 +- 6 files changed, 380 insertions(+), 332 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index ae4ce1f552e8..e7e4f02774f3 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -33,9 +33,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.textfield.TextInputEditText import com.ichi2.anki.CrashReportService import com.ichi2.anki.R import com.ichi2.anki.common.annotations.NeedsTest @@ -48,6 +46,7 @@ import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.KEY_SELECTED_FIELD import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.KEY_SELECTED_LOCALE import com.ichi2.anki.dialogs.LocaleSelectionDialog.Companion.REQUEST_HINT_LOCALE_SELECTION import com.ichi2.anki.launchCatchingTask +import com.ichi2.anki.servicelayer.LanguageHint import com.ichi2.anki.showThemedToast import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.sync.userAcceptsSchemaChange @@ -58,7 +57,9 @@ import com.ichi2.anki.utils.ext.showDialogFragment import com.ichi2.anki.utils.hideKeyboard import com.ichi2.utils.moveCursorToEnd import dev.androidbroadcast.vbpd.viewBinding +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale @@ -73,23 +74,24 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field override fun onNameChanged( position: Int, name: String, + isEditing: Boolean, ) { - launchCatchingTask { - viewModel.rename(position, name) - } + viewModel.rename(position, name, isEditing) } override fun onSortChanged(position: Int) { - launchCatchingTask { - viewModel.changeSort(position) - } + viewModel.changeSort(position) + } + + override fun onLocaleChangeRequested( + position: Int, + languageHint: LanguageHint?, + ) { + localeHintDialog(languageHint, position) } - override fun onLocaleChangeRequested(position: Int) { - val locale = - viewModel.state.value.fields[position] - .locale - localeHintDialog(locale, position) + override fun onRepositionRequested(viewHolder: NoteFieldAdapter.NoteFieldViewHolder) { + touchHelper.startDrag(viewHolder) } } return@lazy NoteFieldAdapter(listener) @@ -103,12 +105,17 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) { var dragFromPosition: Int = RecyclerView.NO_POSITION + override fun isLongPressDragEnabled() = false + override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder, ): Boolean { - viewModel.visuallyReposition(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + val fromPosition = viewHolder.bindingAdapterPosition + val toPosition = target.bindingAdapterPosition + if (fromPosition == RecyclerView.NO_POSITION || toPosition == RecyclerView.NO_POSITION) return false + adapter.submitListMove(fromPosition, toPosition) return true } @@ -118,7 +125,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) { val position = viewHolder.bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { - deleteFieldDialog(position) + deleteFieldDialog(position, adapter.getItem(position).name) // reset transitionX whether the field is deleted or not viewHolder.bindingAdapter?.notifyItemChanged(position) } @@ -129,8 +136,10 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field actionState: Int, ) { super.onSelectedChanged(viewHolder, actionState) - if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + // when viewHolder is moved with focus on edittext, swipe doesn't work correctly + currentFocus?.clearFocus() + hideKeyboard() dragFromPosition = viewHolder?.bindingAdapterPosition ?: RecyclerView.NO_POSITION } } @@ -145,9 +154,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field if (dragFromPosition != RecyclerView.NO_POSITION && dragToPosition != RecyclerView.NO_POSITION && dragFromPosition != dragToPosition ) { - launchCatchingTask { - viewModel.reposition(dragFromPosition, dragToPosition) - } + viewModel.reposition(dragFromPosition, dragToPosition) } this.dragFromPosition = RecyclerView.NO_POSITION @@ -184,58 +191,24 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field } binding.fields.apply { + setHasFixedSize(true) layoutManager = LinearLayoutManager(this@NoteTypeFieldEditor) adapter = this@NoteTypeFieldEditor.adapter touchHelper.attachToRecyclerView(this@apply) } binding.btnAdd.setOnClickListener { addFieldDialog() } onBackPressedDispatcher.addCallback(this) { - val unsavedChange = adapter.unsavedChange - if (unsavedChange != null) { - viewModel.updateByUuid(unsavedChange) - } viewModel.requestDiscardChangesAndClose() } lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { state -> - adapter.submitList(state.fields) - when (state.action) { - is NoteTypeFieldEditorState.Action.Undoable -> { - val message = getString(state.action.resId, *state.action.formatArgs.toTypedArray()) - - showUndoSnackbar(message) - } - is NoteTypeFieldEditorState.Action.Error -> - CrashReportService.sendExceptionReport( - state.action.e.source, - NoteTypeFieldEditor::class.java.simpleName, - ) - is NoteTypeFieldEditorState.Action.Rejected -> - showThemedToast( - this@NoteTypeFieldEditor, - getString(state.action.resId), - true, - ) - is NoteTypeFieldEditorState.Action.SaveRequested -> - showSaveChangesDialog(state.action.isNotUndoable, state.action.isSchemaChanges) - NoteTypeFieldEditorState.Action.DiscardRequested -> - showDiscardChangesDialog() - is NoteTypeFieldEditorState.Action.Close -> { - if (state.action.resId != null) { - showThemedToast( - this@NoteTypeFieldEditor, - getString(state.action.resId), - true, - ) - } - finish() - } - NoteTypeFieldEditorState.Action.None -> { } - } - if (state.action != NoteTypeFieldEditorState.Action.None) { - viewModel.resetAction() + withContext(Dispatchers.Main) { + Timber.d("state: $state") + adapter.submitList(state.fields) + consumeAction(state.action) } + viewModel.resetAction() } } } @@ -244,16 +217,48 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.notetype_field_editor, menu) menu.findItem(R.id.action_save).setOnMenuItemClickListener { - val unsavedChange = adapter.unsavedChange - if (unsavedChange != null) { - viewModel.updateByUuid(unsavedChange) - } viewModel.requestSaveAndClose() true } return true } + fun consumeAction(action: NoteTypeFieldEditorState.Action) { + when (action) { + is NoteTypeFieldEditorState.Action.Undoable -> { + val message = getString(action.resId, *action.formatArgs.toTypedArray()) + + showUndoSnackbar(message) + } + is NoteTypeFieldEditorState.Action.Error -> + CrashReportService.sendExceptionReport( + action.e.source, + NoteTypeFieldEditor::class.java.simpleName, + ) + is NoteTypeFieldEditorState.Action.Rejected -> + showThemedToast( + this@NoteTypeFieldEditor, + getString(action.resId), + true, + ) + is NoteTypeFieldEditorState.Action.SaveRequested -> + showSaveChangesDialog(action.isNotUndoable, action.isSchemaChanges) + NoteTypeFieldEditorState.Action.DiscardRequested -> + showDiscardChangesDialog() + is NoteTypeFieldEditorState.Action.Close -> { + if (action.resId != null) { + showThemedToast( + this@NoteTypeFieldEditor, + getString(action.resId), + true, + ) + } + finish() + } + NoteTypeFieldEditorState.Action.None -> { } + } + } + // ---------------------------------------------------------------------------- // ACTION DIALOGUES // ---------------------------------------------------------------------------- @@ -266,9 +271,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field setAnchorView(findViewById(R.id.btn_add)) isAnchorViewLayoutListenerEnabled = true setAction(R.string.undo) { - launchCatchingTask { - viewModel.undo() - } + viewModel.undo() } } } @@ -326,33 +329,22 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field * Creates a dialog to delete the field * @param position the position of the field */ - private fun deleteFieldDialog(position: Int) { - if (viewModel.state.value.fields.size < 2) { - showThemedToast( - this, - resources.getString(R.string.toast_last_field), - true, - ) - return - } - - val fieldName = - viewModel.state.value.fields[position] - .name + private fun deleteFieldDialog( + position: Int, + fieldName: String, + ) { ConfirmationDialog().let { it.setArgs( title = fieldName, message = resources.getString(R.string.field_delete_warning), ) it.setConfirm { - launchCatchingTask { - viewModel.delete(position) - // This ensures that the context menu closes after the field has been deleted - supportFragmentManager.popBackStackImmediate( - null, - FragmentManager.POP_BACK_STACK_INCLUSIVE, - ) - } + viewModel.delete(position) + // This ensures that the context menu closes after the field has been deleted + supportFragmentManager.popBackStackImmediate( + null, + FragmentManager.POP_BACK_STACK_INCLUSIVE, + ) } showDialogFragment(it) } @@ -379,7 +371,9 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field private class NoteFieldAdapter( private val listener: ItemChangeListener, -) : ListAdapter(DIFF_CALLBACK) { +) : RecyclerView.Adapter() { + val items = mutableListOf() + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, @@ -395,35 +389,94 @@ private class NoteFieldAdapter( holder.bind(getItem(position)) } - override fun onViewDetachedFromWindow(holder: NoteFieldViewHolder) { - holder.detached() + override fun onBindViewHolder( + holder: NoteFieldViewHolder, + position: Int, + payloads: List, + ) { + val changes = payloads.filterIsInstance().toSet() + if (changes.isEmpty()) { + onBindViewHolder(holder, position) + } else { + holder.bind(getItem(position), changes) + } } + fun getItem(position: Int) = items[position] + + override fun getItemCount() = items.size + override fun onViewRecycled(holder: NoteFieldViewHolder) { holder.recycled() } - private var _unsavedChange: NoteTypeFieldRowData? = null + suspend fun submitList(list: List) = + withContext(Dispatchers.Main) { + Timber.d("submitList: $items $list") + val diffResult = + withContext(Dispatchers.Default) { + val diffUtil = NoteTypeFieldDiffUtil(items.toList(), list) + val result = DiffUtil.calculateDiff(diffUtil) + items.clear() + items.addAll(list) + return@withContext result + } + diffResult.dispatchUpdatesTo(this@NoteFieldAdapter) + } - val unsavedChange: NoteTypeFieldRowData? get() { - val change = _unsavedChange - _unsavedChange = null - return change + fun submitListMove( + oldPosition: Int, + newPosition: Int, + ) { + val field = items.removeAt(oldPosition) + items.add(newPosition, field) + notifyItemMoved(oldPosition, newPosition) } - companion object { - private val DIFF_CALLBACK = - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: NoteTypeFieldRowData, - newItem: NoteTypeFieldRowData, - ) = oldItem.uuid == newItem.uuid - - override fun areContentsTheSame( - oldItem: NoteTypeFieldRowData, - newItem: NoteTypeFieldRowData, - ) = oldItem == newItem + class NoteTypeFieldDiffUtil( + private val oldList: List, + private val newList: List, + ) : DiffUtil.Callback() { + override fun getOldListSize() = oldList.size + + override fun getNewListSize() = newList.size + + override fun areItemsTheSame( + oldItemPosition: Int, + newItemPosition: Int, + ) = oldList[oldItemPosition].uuid == newList[newItemPosition].uuid || oldList[oldItemPosition].name == newList[newItemPosition].name + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int, + ) = oldList[oldItemPosition] == newList[newItemPosition] + + override fun getChangePayload( + oldItemPosition: Int, + newItemPosition: Int, + ): Any? { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + Timber.d("payload: $oldItemPosition, $newItemPosition") + Timber.d("payload: $oldItem, $newItem") + return when { + oldItem.name != newItem.name -> Payload.Rename + oldItem.isOrder != newItem.isOrder -> Payload.Sort + oldItem.locale != newItem.locale -> Payload.Locale + else -> super.getChangePayload(oldItemPosition, newItemPosition) } + } + + enum class Payload { + Rename, + Sort, + Locale, + ; + + companion object { + val entriesSet = entries.toSet() + } + } } inner class NoteFieldViewHolder( @@ -436,38 +489,39 @@ private class NoteFieldAdapter( isFocusable = true isFocusableInTouchMode = true } + fieldDragHandle.setOnLongClickListener { _ -> + listener.onRepositionRequested(this@NoteFieldViewHolder) + return@setOnLongClickListener true + } fieldSortButton.setOnClickListener { val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { + clozeImeAndClearFocus() listener.onSortChanged(position) } } fieldLanguageButton.setOnClickListener { val position = bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { - listener.onLocaleChangeRequested(position) + clozeImeAndClearFocus() + val locale = getItem(position).locale + listener.onLocaleChangeRequested(position, locale) } } fieldEdit.apply { - setOnFocusChangeListener { v, _ -> - moveCursorToEnd() - hideKeyboard() - val position = bindingAdapterPosition - if (position != RecyclerView.NO_POSITION) { - val name = (v as TextInputEditText).text?.toString() - if (!name.isNullOrBlank()) { - listener.onNameChanged(position, name) - _unsavedChange = null - } else { - setText((bindingAdapter as NoteFieldAdapter).getItem(bindingAdapterPosition).name) - } - } + setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) return@setOnFocusChangeListener + save() + clozeImeAndClearFocus() } setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - root.requestFocus() + return@setOnEditorActionListener when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + clearFocus() + true + } + else -> false } - false } addTextChangedListener( object : TextWatcher { @@ -485,61 +539,81 @@ private class NoteFieldAdapter( count: Int, end: Int, ) { - if (bindingAdapterPosition != RecyclerView.NO_POSITION) { - val unsavedChange = - (bindingAdapter as NoteFieldAdapter) - .getItem( - bindingAdapterPosition, - ).copy( - name = s?.toString().orEmpty(), - ) - _unsavedChange = unsavedChange - } } - override fun afterTextChanged(s: Editable?) { } + override fun afterTextChanged(s: Editable?) { + save() + } }, ) } - fieldEditLayout.setEndIconOnClickListener { - val position = bindingAdapterPosition - if (position != RecyclerView.NO_POSITION) { - fieldEdit.setText((bindingAdapter as NoteFieldAdapter).getItem(position).name) - fieldEdit.moveCursorToEnd() - } - fieldEditLayout.isEndIconVisible = false - } } } - fun bind(item: NoteTypeFieldRowData) { + fun bind( + item: NoteTypeFieldRowData, + payload: Set = NoteTypeFieldDiffUtil.Payload.entriesSet, + ) { + Timber.d("bind: $item at $bindingAdapterPosition ") binding.apply { - if (fieldEdit.text.toString() != item.name) { - fieldEdit.setText(item.name) + if (payload.contains(NoteTypeFieldDiffUtil.Payload.Rename) && item.name != fieldEdit.text?.toString().orEmpty()) { + Timber.d("field edittext: ${fieldEdit.text} to ${item.name} at $bindingAdapterPosition") + setText(item.name) + } + if (payload.contains(NoteTypeFieldDiffUtil.Payload.Sort)) { + fieldSortButton.isChecked = item.isOrder + } + if (payload.contains(NoteTypeFieldDiffUtil.Payload.Locale)) { + fieldLanguageButton.isChecked = item.locale != null } - fieldEditLayout.isEndIconVisible = false - fieldSortButton.isChecked = item.isOrder - fieldLanguageButton.isChecked = item.locale != null } } - fun detached() { - binding.fieldEdit.clearFocus() - } - fun recycled() { binding.root.translationX = 0f } + + fun setText(name: String) { + binding.fieldEdit.apply { + setText(name) + if (hasFocus()) { + moveCursorToEnd() + } + } + } + + fun save() { + val position = bindingAdapterPosition + if (position == RecyclerView.NO_POSITION) return + val newName = + binding.fieldEdit.text + ?.toString() + .orEmpty() + listener.onNameChanged(position, newName, binding.fieldEdit.hasFocus()) + } + + fun clozeImeAndClearFocus() { + binding.fieldEdit.hideKeyboard() + if (binding.fieldEdit.hasFocus()) { + binding.root.requestFocus() + } + } } interface ItemChangeListener { fun onNameChanged( position: Int, name: String, + isEditing: Boolean, ) fun onSortChanged(position: Int) - fun onLocaleChangeRequested(position: Int) + fun onLocaleChangeRequested( + position: Int, + languageHint: LanguageHint?, + ) + + fun onRepositionRequested(viewHolder: NoteFieldViewHolder) } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index cd05ae593823..5d243fe4c480 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -10,7 +10,6 @@ import anki.notetypes.NotetypeKt import anki.notetypes.copy import com.ichi2.anki.CollectionManager.withCol import com.ichi2.anki.R -import com.ichi2.anki.common.utils.ext.indexOfOrNull import com.ichi2.anki.libanki.Collection import com.ichi2.anki.libanki.NotetypeJson import com.ichi2.anki.libanki.Notetypes @@ -24,9 +23,9 @@ import com.ichi2.anki.servicelayer.LanguageHint import com.ichi2.anki.servicelayer.LanguageHintService import com.ichi2.anki.servicelayer.LanguageHintService.languageHint import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -42,19 +41,29 @@ class NoteTypeFieldEditorViewModel( savedStateHandle: SavedStateHandle, ) : ViewModel() { private val ntid = savedStateHandle.get(EXTRA_NOTETYPE_ID)!! - private val fieldsEditOperationStack = - savedStateHandle.getMutableStateFlow( - KEY_FIELD_EDIT_OPERATION, - listOf(), - ) + private val fieldsEditOperationStack = MutableStateFlow(listOf()) /** - * Checks if there are unsaved undoable changes or no changes + * Checks if there are unsaved undoable changes */ - private val isNotUndoable get() = fieldsEditOperationStack.value.any { !it.isUndoable } || fieldsEditOperationStack.value.isEmpty() - + private val hasUndoableOperation get() = fieldsEditOperationStack.value.any { !it.isUndoable } private val _state = MutableStateFlow(NoteTypeFieldEditorState(fields = emptyList())) - val state: StateFlow = _state.asStateFlow() + + /** + * The pair of the field uuid and the new name which is pending confirmation + */ + private val pendingRename = MutableStateFlow("" to "") + val state: Flow = + _state.combine(pendingRename) { state, (uuid, name) -> + Timber.d("current state: $state") + val mutableFields = state.fields.toMutableList() + val pos = mutableFields.indexOfFirst { it.uuid == uuid } + if (pos != NO_POSITION) { + Timber.d("pending rename: override ${mutableFields[pos].name} to $name") + mutableFields[pos] = mutableFields[pos].copy(name = name) + } + return@combine state.copy(fields = mutableFields.toList()) + } init { viewModelScope.launch { @@ -65,15 +74,17 @@ class NoteTypeFieldEditorViewModel( /** * Creates new field with the given name * @param position the position of the new field or [NO_POSITION] to add to the end - * @param name the name of the field, which must be unique, not empty, allowed to the notetype + * @param name the name of the field, which must be unique, not empty, allowed for the notetype */ fun add( position: Int = NO_POSITION, name: String, ) { + Timber.d("addOperationStackAddField") + Timber.d("add $name at $position") uniqueName(name = name).fold( onSuccess = { validName -> - val position = if (position == NO_POSITION) state.value.fields.lastIndex + 1 else position + val position = if (position == NO_POSITION) _state.value.fields.lastIndex + 1 else position _state.update { oldValue -> val fields = oldValue.fields.toMutableList() fields.temporaryAdd(position, validName) @@ -89,7 +100,7 @@ class NoteTypeFieldEditorViewModel( }, onFailure = { resId -> val action = NoteTypeFieldEditorState.Action.Rejected(resId) - _state.value = state.value.copy(action = action) + _state.value = _state.value.copy(action = action) }, ) } @@ -97,45 +108,78 @@ class NoteTypeFieldEditorViewModel( /** * Renames the existing field with the given name * + * * @param position the position of the field to rename * @param name the name of the field + * @param isEditing true if the user is still typing. If true, undo snackbar doesn't appear. */ fun rename( position: Int, name: String, + isEditing: Boolean, ) { - val oldName = state.value.fields[position].name - if (oldName == name) return - uniqueName(name).fold( - onSuccess = { validName -> - val oldName = state.value.fields[position].name - _state.update { oldValue -> + Timber.d("addOperationStackRenameField") + Timber.d("isEditing: $isEditing") + val mutableStack = fieldsEditOperationStack.value.toMutableList() + while (true) { + // remove previous rename operation + val last = mutableStack.lastOrNull() + if (last is NoteTypeFieldOperation.Rename && last.position == position) { + mutableStack.removeAt(mutableStack.lastIndex) + } else { + break + } + } + + val oldValue = _state.value + val oldField = oldValue.fields[position] + val oldName = oldField.name + val result = uniqueName(position, name) + + Timber.d("rename $oldName to $name at $position") + pendingRename.value = oldField.uuid to name + if (isEditing) { + if (result is UniqueNameResult.Success && oldName == result.name) { + val operation = NoteTypeFieldOperation.Rename(position, oldName, result.name) + mutableStack.add(operation) + } + fieldsEditOperationStack.value = mutableStack.toList() + + val action = NoteTypeFieldEditorState.Action.None + _state.value = oldValue.copy(action = action) + } else { + pendingRename.value = "" to "" + when (result) { + is UniqueNameResult.Success -> { + if (oldName == result.name) { + Timber.d("rename cancelled at $position") + return + } + val operation = NoteTypeFieldOperation.Rename(position, oldName, result.name) + mutableStack.add(operation) + fieldsEditOperationStack.value = mutableStack.toList() + val fields = oldValue.fields.toMutableList() - fields.temporaryRename(position, validName) + fields.temporaryRename(position, result.name) val action = NoteTypeFieldEditorState.Action.Undoable( R.string.model_field_editor_rename_success_result, - arrayListOf(oldName, validName), + arrayListOf(oldName, result.name), ) - return@update oldValue.copy(fields = fields.toList(), action = action) + _state.value = oldValue.copy(fields = fields.toList(), action = action) } - - val operation = NoteTypeFieldOperation.Rename(position, oldName, validName) - fieldsEditOperationStack.update { stack -> - val mutableStack = stack.toMutableList() - mutableStack.add(operation) - return@update mutableStack.toList() - } - }, - onFailure = { resId -> - _state.update { oldValue -> - val fields = oldValue.fields.toMutableList() - fields.temporaryRefresh(position) - val action = NoteTypeFieldEditorState.Action.Rejected(resId) - return@update oldValue.copy(fields = fields.toList(), action = action) + is UniqueNameResult.Failure -> { + Timber.d("rename failed due to $result at $position") + val action = + if (oldName == name) { + NoteTypeFieldEditorState.Action.None + } else { + NoteTypeFieldEditorState.Action.Rejected(result.resId) + } + _state.value = oldValue.copy(action = action) } - }, - ) + } + } } /** @@ -143,20 +187,25 @@ class NoteTypeFieldEditorViewModel( * @param position the position of the field to delete */ fun delete(position: Int) { - val isLast = position == state.value.fields.lastIndex + Timber.d("addOperationStackDeleteField") + val isLast = position == _state.value.fields.lastIndex if (position == 0 && isLast) { val action = NoteTypeFieldEditorState.Action.Rejected(R.string.toast_last_field) - _state.value = state.value.copy(action = action) + _state.value = _state.value.copy(action = action) + Timber.d("delete failed: cannot delete the only remaining field") return } - val fieldData = state.value.fields[position] + val fieldData = _state.value.fields[position] + Timber.d("delete ${fieldData.name} at $position") _state.update { oldValue -> val fields = oldValue.fields.toMutableList() fields.temporaryDelete(position) - if (isLast) { - fields.temporaryChangeSort(position - 1) + if (fieldData.isOrder) { + val newPosition = if (isLast) position - 1 else position + Timber.d("change sort field from $position to $newPosition because of delete") + fields.temporaryChangeSort(newPosition) } val action = NoteTypeFieldEditorState.Action.Undoable( @@ -174,44 +223,21 @@ class NoteTypeFieldEditorViewModel( } /** - * Moves the existing field to the given position in ViewState - * - * This method does NOT record changes - * reposition() must be called after calling visuallyReposition() before calling any other methods - * - * @param oldPosition the current position of the target field - * @param newPosition the new position of the target field - * @see NoteTypeFieldEditorViewModel.reposition - */ - fun visuallyReposition( - oldPosition: Int, - newPosition: Int, - ) { - _state.update { oldValue -> - val fields = oldValue.fields.toMutableList() - fields.temporaryReposition(oldPosition, newPosition) - val action = NoteTypeFieldEditorState.Action.None - return@update oldValue.copy(fields = fields.toList(), action = action) - } - } - - /** - * Moves the existing field to the given position in ViewState - * - * This method DOES record changes - * visuallyReposition() must be called before calling visuallyReposition() + * Moves the existing field to the given position * * @param oldPosition the position of the target field before repositioning it * @param newPosition the new position of the target field - * @see NoteTypeFieldEditorViewModel.visuallyReposition */ fun reposition( oldPosition: Int, newPosition: Int, ) { - // fields has been repositioned by visuallyReposition(). + Timber.d("addOperationStackRepositionField") + Timber.d("reposition $oldPosition to $newPosition") _state.update { oldValue -> - val name = oldValue.fields[newPosition].name + val fields = oldValue.fields.toMutableList() + val name = oldValue.fields[oldPosition].name + fields.temporaryReposition(oldPosition, newPosition) val action = NoteTypeFieldEditorState.Action.Undoable( R.string.model_field_editor_reposition_success_result, @@ -221,7 +247,7 @@ class NoteTypeFieldEditorViewModel( newPosition + 1, ), ) - return@update oldValue.copy(action = action) + return@update oldValue.copy(fields = fields.toList(), action = action) } val operation = NoteTypeFieldOperation.Reposition(oldPosition, newPosition) fieldsEditOperationStack.update { @@ -236,12 +262,14 @@ class NoteTypeFieldEditorViewModel( * @param position the position of the target field */ fun changeSort(position: Int) { - val fields = state.value.fields + Timber.d("addOperationStackChangeSortField") + Timber.d("changeSort to $position") + val fields = _state.value.fields val oldPosition = fields.indexOfFirst { it.isOrder } val name = fields[position].name if (oldPosition == position) { val action = NoteTypeFieldEditorState.Action.None - _state.value = state.value.copy(action = action) + _state.value = _state.value.copy(action = action) return } _state.update { oldValue -> @@ -269,10 +297,12 @@ class NoteTypeFieldEditorViewModel( position: Int, locale: LanguageHint?, ) { - val oldLocale = state.value.fields[position].locale + Timber.d("addOperationStackSetLanguageHint") + Timber.d("setLanguageHint to $locale at $position") + val oldLocale = _state.value.fields[position].locale if (oldLocale == locale) { val action = NoteTypeFieldEditorState.Action.None - _state.value = state.value.copy(action = action) + _state.value = _state.value.copy(action = action) return } _state.update { oldValue -> @@ -298,43 +328,6 @@ class NoteTypeFieldEditorViewModel( } } - /** - * Updates the field with the given row data referring to the uuid of the data - * @param rowData the row data of the field - * @param position the new position of the field or [NO_POSITION] to add to the end or not to move the field - */ - fun updateByUuid( - rowData: NoteTypeFieldRowData, - position: Int = NO_POSITION, - ) { - val fieldIndex = state.value.fields.indexOfOrNull { it.uuid == rowData.uuid } - val isNew = fieldIndex == null - val position = - if (position == NO_POSITION) { - fieldIndex - ?: (state.value.fields.lastIndex + 1) - } else { - position - } - if (isNew) { - add(position, rowData.name) - val fields = state.value.fields.toMutableList() - // Update the uuid of the new field - // This is a new field so it has a unique uuid - fields[position] = fields[position].copy(uuid = rowData.uuid) - _state.value = state.value.copy(fields = state.value.fields.toList()) - } - - val field = state.value.fields[position] - if (field.name != rowData.name) rename(position, rowData.name) - if (!isNew && fieldIndex != position) { - visuallyReposition(fieldIndex, position) - reposition(fieldIndex, position) - } - if (field.isOrder != rowData.isOrder) changeSort(position) - if (field.locale != rowData.locale) setLanguageHint(position, rowData.locale) - } - /** * Obtains the field list from [Collection] and refresh the state */ @@ -402,8 +395,6 @@ class NoteTypeFieldEditorViewModel( this[position] = field.copy(locale = locale) } - private fun MutableList.temporaryRefresh(position: Int) = temporaryUpdateUuid(position) - private fun MutableList.temporaryUpdateUuid( position: Int, uuid: String = UUID.randomUUID().toString(), @@ -414,11 +405,15 @@ class NoteTypeFieldEditorViewModel( /** * Cleans the input field or explain why it's rejected + * @param position the position of the field * @param name the input * @return the result UniqueNameResult.Success which contains the unique name or UniqueNameResult.Failure which contains string resource id of the reason why it's rejected * */ - private fun uniqueName(name: String): UniqueNameResult { + private fun uniqueName( + position: Int = NO_POSITION, + name: String, + ): UniqueNameResult { var input = name .replace("[\\n\\r{}:\"]".toRegex(), "") @@ -432,10 +427,11 @@ class NoteTypeFieldEditorViewModel( } input = input.substring(offset).trim() if (input.isEmpty()) { - return UniqueNameResult.Failure(R.string.toast_empty_name) + return UniqueNameResult.Failure.EmptyName } - if (state.value.fields.any { it.name == input }) { - return UniqueNameResult.Failure(R.string.toast_duplicate_field) + val otherFields = _state.value.fields.filterIndexed { index, _ -> index != position } + if (otherFields.any { it.name == input }) { + return UniqueNameResult.Failure.DuplicateName } return UniqueNameResult.Success(input) } @@ -500,7 +496,7 @@ class NoteTypeFieldEditorViewModel( force -> NoteTypeFieldEditorState.Action.Close(R.string.model_field_editor_discard_success_result) else -> NoteTypeFieldEditorState.Action.DiscardRequested } - _state.value = state.value.copy(action = action) + _state.value = _state.value.copy(action = action) } /** @@ -513,19 +509,19 @@ class NoteTypeFieldEditorViewModel( val isSchemaChange = fieldsEditOperationStack.value.any { it.isSchemaChange } if (fieldsEditOperationStack.value.isEmpty()) { val action = NoteTypeFieldEditorState.Action.Close() - _state.value = state.value.copy(action = action) - } else if (force || (!isSchemaChange && !isNotUndoable)) { + _state.value = _state.value.copy(action = action) + } else if (force || (!isSchemaChange && !hasUndoableOperation)) { viewModelScope.launch { val action = save().fold( onSuccess = { NoteTypeFieldEditorState.Action.Close(R.string.model_field_editor_save_success_result) }, onFailure = { NoteTypeFieldEditorState.Action.Error(ReportableException(it, it !is BackendException)) }, ) - _state.value = state.value.copy(action = action) + _state.value = _state.value.copy(action = action) } } else { - val action = NoteTypeFieldEditorState.Action.SaveRequested(isNotUndoable, isSchemaChange) - _state.value = state.value.copy(action = action) + val action = NoteTypeFieldEditorState.Action.SaveRequested(hasUndoableOperation, isSchemaChange) + _state.value = _state.value.copy(action = action) } } @@ -535,14 +531,14 @@ class NoteTypeFieldEditorViewModel( * used when the current action is consumed in the side of UI */ fun resetAction() { - val action = NoteTypeFieldEditorState.Action.None - _state.value = state.value.copy(action = action) + val actionNone = NoteTypeFieldEditorState.Action.None + _state.value = _state.value.copy(action = actionNone) } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) suspend fun save() = withContext(Dispatchers.IO) { - return@withContext if (isNotUndoable) { + return@withContext if (hasUndoableOperation) { saveIrreversible() } else { saveReversible() @@ -773,7 +769,6 @@ class NoteTypeFieldEditorViewModel( } private companion object { - private const val KEY_FIELD_EDIT_OPERATION = "key_field_edit_operation" private const val NO_POSITION = -1 } @@ -785,12 +780,16 @@ class NoteTypeFieldEditorViewModel( val name: String, ) : UniqueNameResult() - data class Failure( + sealed class Failure( /** * The string resource id of the reason why the name is rejected */ @StringRes val resId: Int, - ) : UniqueNameResult() + ) : UniqueNameResult() { + object EmptyName : Failure(R.string.toast_empty_name) + + object DuplicateName : Failure(R.string.toast_duplicate_field) + } @OptIn(ExperimentalContracts::class) @Contract diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt index eb2f9742eeb8..f3ca1f571bf0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt @@ -1,12 +1,8 @@ package com.ichi2.anki.notetype.fieldeditor -import android.os.Parcelable -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize import java.util.Locale -@Parcelize -sealed interface NoteTypeFieldOperation : Parcelable { +sealed interface NoteTypeFieldOperation { val isUndoable: Boolean val isSchemaChange: Boolean @@ -14,10 +10,8 @@ sealed interface NoteTypeFieldOperation : Parcelable { val position: Int, val name: String, ) : NoteTypeFieldOperation { - @IgnoredOnParcel override val isUndoable = true - @IgnoredOnParcel override val isSchemaChange = true } @@ -26,10 +20,8 @@ sealed interface NoteTypeFieldOperation : Parcelable { val oldName: String, val newName: String, ) : NoteTypeFieldOperation { - @IgnoredOnParcel override val isUndoable = true - @IgnoredOnParcel override val isSchemaChange = false } @@ -38,10 +30,8 @@ sealed interface NoteTypeFieldOperation : Parcelable { val fieldData: NoteTypeFieldRowData, val isLast: Boolean, ) : NoteTypeFieldOperation { - @IgnoredOnParcel override val isUndoable = true - @IgnoredOnParcel override val isSchemaChange = false } @@ -49,10 +39,8 @@ sealed interface NoteTypeFieldOperation : Parcelable { val oldPosition: Int, val newPosition: Int, ) : NoteTypeFieldOperation { - @IgnoredOnParcel override val isUndoable = true - @IgnoredOnParcel override val isSchemaChange = true } @@ -60,10 +48,8 @@ sealed interface NoteTypeFieldOperation : Parcelable { val oldPosition: Int, val newPosition: Int, ) : NoteTypeFieldOperation { - @IgnoredOnParcel override val isUndoable = true - @IgnoredOnParcel override val isSchemaChange = true } @@ -72,10 +58,8 @@ sealed interface NoteTypeFieldOperation : Parcelable { val oldLocale: Locale?, val newLocale: Locale?, ) : NoteTypeFieldOperation { - @IgnoredOnParcel override val isUndoable = false - @IgnoredOnParcel override val isSchemaChange = true } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt index c795099e3a70..e13c5a7cc9d4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt @@ -1,14 +1,11 @@ package com.ichi2.anki.notetype.fieldeditor -import android.os.Parcelable -import kotlinx.parcelize.Parcelize import java.util.Locale import java.util.UUID -@Parcelize data class NoteTypeFieldRowData( val uuid: String = UUID.randomUUID().toString(), val name: String, val isOrder: Boolean = false, val locale: Locale? = null, -) : Parcelable +) diff --git a/AnkiDroid/src/main/res/layout/item_notetype_field.xml b/AnkiDroid/src/main/res/layout/item_notetype_field.xml index 9c4c88bb3e82..f455f68e673c 100644 --- a/AnkiDroid/src/main/res/layout/item_notetype_field.xml +++ b/AnkiDroid/src/main/res/layout/item_notetype_field.xml @@ -1,33 +1,28 @@ - + android:minHeight="?android:attr/listPreferredItemHeight" + android:orientation="horizontal"> + android:layout_gravity="center" /> - + android:layout_gravity="center" /> + diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt index 15537a864919..04a052a21bc9 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/NoteTypeFieldEditorTest.kt @@ -128,7 +128,12 @@ class NoteTypeFieldEditorTest( ) when (fieldOperationType) { FieldOperationType.ADD_FIELD -> noteTypeFieldEditor.viewModel.add(name = fieldName) - FieldOperationType.RENAME_FIELD -> noteTypeFieldEditor.viewModel.rename(position, fieldName) + FieldOperationType.RENAME_FIELD -> + noteTypeFieldEditor.viewModel.rename( + position, + fieldName, + false, + ) } noteTypeFieldEditor.lifecycleScope.launch { noteTypeFieldEditor.viewModel.save() From 4be6a67e2184fa626e78fde168b6cdd084b94300 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:57:12 +0900 Subject: [PATCH 12/19] fix(notetypefieldeditor): update recyclerview list in UI thread Ensure thread safety during diffing. --- .../fieldeditor/NoteTypeFieldEditor.kt | 30 +++++++++---------- .../NoteTypeFieldEditorViewModel.kt | 15 ++++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index e7e4f02774f3..225db51ff618 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -127,7 +127,6 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field if (position != RecyclerView.NO_POSITION) { deleteFieldDialog(position, adapter.getItem(position).name) // reset transitionX whether the field is deleted or not - viewHolder.bindingAdapter?.notifyItemChanged(position) } } @@ -346,6 +345,10 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field FragmentManager.POP_BACK_STACK_INCLUSIVE, ) } + it.setCancel { + viewModel.refreshAt(position) + } + it.isCancelable = false showDialogFragment(it) } } @@ -372,7 +375,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field private class NoteFieldAdapter( private val listener: ItemChangeListener, ) : RecyclerView.Adapter() { - val items = mutableListOf() + var items = emptyList() override fun onCreateViewHolder( parent: ViewGroup, @@ -406,21 +409,17 @@ private class NoteFieldAdapter( override fun getItemCount() = items.size - override fun onViewRecycled(holder: NoteFieldViewHolder) { - holder.recycled() - } - suspend fun submitList(list: List) = withContext(Dispatchers.Main) { + val oldList = items.toList() Timber.d("submitList: $items $list") val diffResult = withContext(Dispatchers.Default) { - val diffUtil = NoteTypeFieldDiffUtil(items.toList(), list) + val diffUtil = NoteTypeFieldDiffUtil(oldList, list) val result = DiffUtil.calculateDiff(diffUtil) - items.clear() - items.addAll(list) return@withContext result } + items = list diffResult.dispatchUpdatesTo(this@NoteFieldAdapter) } @@ -428,8 +427,12 @@ private class NoteFieldAdapter( oldPosition: Int, newPosition: Int, ) { - val field = items.removeAt(oldPosition) - items.add(newPosition, field) + items = + buildList { + addAll(items) + val field = removeAt(oldPosition) + add(newPosition, field) + } notifyItemMoved(oldPosition, newPosition) } @@ -555,6 +558,7 @@ private class NoteFieldAdapter( payload: Set = NoteTypeFieldDiffUtil.Payload.entriesSet, ) { Timber.d("bind: $item at $bindingAdapterPosition ") + binding.root.translationX = 0f binding.apply { if (payload.contains(NoteTypeFieldDiffUtil.Payload.Rename) && item.name != fieldEdit.text?.toString().orEmpty()) { Timber.d("field edittext: ${fieldEdit.text} to ${item.name} at $bindingAdapterPosition") @@ -569,10 +573,6 @@ private class NoteFieldAdapter( } } - fun recycled() { - binding.root.translationX = 0f - } - fun setText(name: String) { binding.fieldEdit.apply { setText(name) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index 5d243fe4c480..7be7bc486d94 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -328,6 +328,21 @@ class NoteTypeFieldEditorViewModel( } } + /** + * Refreshes the uuid of the field at the given position + * + * This is used to force a UI update for a specific item even when its data + * hasn't logically changed. + * @param position the position of the field + */ + fun refreshAt(position: Int) { + _state.update { oldValue -> + val fields = oldValue.fields.toMutableList() + fields.temporaryUpdateUuid(position) + return@update oldValue.copy(fields = fields.toList()) + } + } + /** * Obtains the field list from [Collection] and refresh the state */ From 78a19370281cd6c5f44104936ce76e2129f2af24 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:21:07 +0900 Subject: [PATCH 13/19] fix(notetypefieldeditor): prevent field from being deleted without user's confirmation --- .../ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt index f3ca1f571bf0..667d0804aacd 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldOperation.kt @@ -32,7 +32,7 @@ sealed interface NoteTypeFieldOperation { ) : NoteTypeFieldOperation { override val isUndoable = true - override val isSchemaChange = false + override val isSchemaChange = true } data class ChangeSort( From e59df35709cd2a162c663ed092f88f9e0b411958 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sat, 14 Mar 2026 22:34:46 +0900 Subject: [PATCH 14/19] refactor: simplify field editor and remove redundant strings Removed the confirmation dialog when deleting a field. Disabled individual undo actions for field operations except for delete. Cleaned up strings. --- .../fieldeditor/NoteTypeFieldEditor.kt | 33 +------------ .../NoteTypeFieldEditorViewModel.kt | 46 ++----------------- .../main/res/values-af/17-model-manager.xml | 2 - .../main/res/values-am/17-model-manager.xml | 2 - .../main/res/values-ar/17-model-manager.xml | 2 - .../main/res/values-az/17-model-manager.xml | 2 - .../main/res/values-be/17-model-manager.xml | 2 - .../main/res/values-bg/17-model-manager.xml | 2 - .../main/res/values-bn/17-model-manager.xml | 2 - .../main/res/values-ca/17-model-manager.xml | 2 - .../main/res/values-ckb/17-model-manager.xml | 2 - .../main/res/values-cs/17-model-manager.xml | 2 - .../main/res/values-da/17-model-manager.xml | 2 - .../main/res/values-de/17-model-manager.xml | 2 - .../main/res/values-el/17-model-manager.xml | 2 - .../main/res/values-eo/17-model-manager.xml | 2 - .../res/values-es-rAR/17-model-manager.xml | 2 - .../res/values-es-rES/17-model-manager.xml | 2 - .../main/res/values-et/17-model-manager.xml | 2 - .../main/res/values-eu/17-model-manager.xml | 2 - .../main/res/values-fa/17-model-manager.xml | 2 - .../main/res/values-fi/17-model-manager.xml | 2 - .../main/res/values-fil/17-model-manager.xml | 2 - .../main/res/values-fr/17-model-manager.xml | 2 - .../main/res/values-fy/17-model-manager.xml | 2 - .../main/res/values-ga/17-model-manager.xml | 2 - .../main/res/values-gl/17-model-manager.xml | 2 - .../main/res/values-got/17-model-manager.xml | 2 - .../main/res/values-gu/17-model-manager.xml | 2 - .../main/res/values-heb/17-model-manager.xml | 2 - .../main/res/values-hi/17-model-manager.xml | 2 - .../main/res/values-hr/17-model-manager.xml | 2 - .../main/res/values-hu/17-model-manager.xml | 2 - .../main/res/values-hy/17-model-manager.xml | 2 - .../main/res/values-ind/17-model-manager.xml | 2 - .../main/res/values-it/17-model-manager.xml | 2 - .../main/res/values-iw/17-model-manager.xml | 2 - .../main/res/values-ja/17-model-manager.xml | 2 - .../main/res/values-ka/17-model-manager.xml | 2 - .../main/res/values-kk/17-model-manager.xml | 2 - .../main/res/values-km/17-model-manager.xml | 2 - .../main/res/values-kn/17-model-manager.xml | 2 - .../main/res/values-ko/17-model-manager.xml | 2 - .../main/res/values-ku/17-model-manager.xml | 2 - .../main/res/values-ky/17-model-manager.xml | 2 - .../main/res/values-lt/17-model-manager.xml | 2 - .../main/res/values-lv/17-model-manager.xml | 2 - .../main/res/values-mk/17-model-manager.xml | 2 - .../main/res/values-ml/17-model-manager.xml | 2 - .../main/res/values-mn/17-model-manager.xml | 2 - .../main/res/values-mr/17-model-manager.xml | 2 - .../main/res/values-ms/17-model-manager.xml | 2 - .../main/res/values-my/17-model-manager.xml | 2 - .../main/res/values-nl/17-model-manager.xml | 2 - .../main/res/values-nn/17-model-manager.xml | 2 - .../main/res/values-no/17-model-manager.xml | 2 - .../main/res/values-or/17-model-manager.xml | 2 - .../main/res/values-pa/17-model-manager.xml | 2 - .../main/res/values-pl/17-model-manager.xml | 2 - .../res/values-pt-rBR/17-model-manager.xml | 2 - .../res/values-pt-rPT/17-model-manager.xml | 2 - .../main/res/values-ro/17-model-manager.xml | 2 - .../main/res/values-ru/17-model-manager.xml | 2 - .../main/res/values-sat/17-model-manager.xml | 2 - .../main/res/values-sc/17-model-manager.xml | 2 - .../main/res/values-sk/17-model-manager.xml | 2 - .../main/res/values-sl/17-model-manager.xml | 2 - .../main/res/values-sq/17-model-manager.xml | 2 - .../main/res/values-sr/17-model-manager.xml | 2 - .../main/res/values-sv/17-model-manager.xml | 2 - .../main/res/values-ta/17-model-manager.xml | 2 - .../main/res/values-te/17-model-manager.xml | 2 - .../main/res/values-tgl/17-model-manager.xml | 2 - .../main/res/values-th/17-model-manager.xml | 2 - .../main/res/values-ti/17-model-manager.xml | 2 - .../main/res/values-tr/17-model-manager.xml | 2 - .../main/res/values-tt/17-model-manager.xml | 2 - .../main/res/values-ug/17-model-manager.xml | 2 - .../main/res/values-uk/17-model-manager.xml | 2 - .../main/res/values-ur/17-model-manager.xml | 2 - .../main/res/values-uz/17-model-manager.xml | 2 - .../main/res/values-vi/17-model-manager.xml | 2 - .../res/values-zh-rCN/17-model-manager.xml | 2 - .../res/values-zh-rTW/17-model-manager.xml | 2 - .../src/main/res/values/17-model-manager.xml | 8 ---- 85 files changed, 6 insertions(+), 245 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 225db51ff618..5fdb8ebb810e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -26,7 +26,6 @@ import android.view.inputmethod.EditorInfo import androidx.activity.addCallback import androidx.activity.viewModels import androidx.core.os.BundleCompat -import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -125,8 +124,7 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field ) { val position = viewHolder.bindingAdapterPosition if (position != RecyclerView.NO_POSITION) { - deleteFieldDialog(position, adapter.getItem(position).name) - // reset transitionX whether the field is deleted or not + viewModel.delete(position) } } @@ -324,35 +322,6 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field } } - /** - * Creates a dialog to delete the field - * @param position the position of the field - */ - private fun deleteFieldDialog( - position: Int, - fieldName: String, - ) { - ConfirmationDialog().let { - it.setArgs( - title = fieldName, - message = resources.getString(R.string.field_delete_warning), - ) - it.setConfirm { - viewModel.delete(position) - // This ensures that the context menu closes after the field has been deleted - supportFragmentManager.popBackStackImmediate( - null, - FragmentManager.POP_BACK_STACK_INCLUSIVE, - ) - } - it.setCancel { - viewModel.refreshAt(position) - } - it.isCancelable = false - showDialogFragment(it) - } - } - /** * Creates a dialog to show the available locale list for the field * @param locale the current locale of the field diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index 7be7bc486d94..d2f0d4e27ad1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -88,7 +88,7 @@ class NoteTypeFieldEditorViewModel( _state.update { oldValue -> val fields = oldValue.fields.toMutableList() fields.temporaryAdd(position, validName) - val action = NoteTypeFieldEditorState.Action.Undoable(R.string.model_field_editor_add_success_result, listOf(validName)) + val action = NoteTypeFieldEditorState.Action.None return@update oldValue.copy(fields = fields.toList(), action = action) } val operation = NoteTypeFieldOperation.Add(position, validName) @@ -162,10 +162,7 @@ class NoteTypeFieldEditorViewModel( val fields = oldValue.fields.toMutableList() fields.temporaryRename(position, result.name) val action = - NoteTypeFieldEditorState.Action.Undoable( - R.string.model_field_editor_rename_success_result, - arrayListOf(oldName, result.name), - ) + NoteTypeFieldEditorState.Action.None _state.value = oldValue.copy(fields = fields.toList(), action = action) } is UniqueNameResult.Failure -> { @@ -236,17 +233,9 @@ class NoteTypeFieldEditorViewModel( Timber.d("reposition $oldPosition to $newPosition") _state.update { oldValue -> val fields = oldValue.fields.toMutableList() - val name = oldValue.fields[oldPosition].name fields.temporaryReposition(oldPosition, newPosition) val action = - NoteTypeFieldEditorState.Action.Undoable( - R.string.model_field_editor_reposition_success_result, - arrayListOf( - name, - oldPosition + 1, - newPosition + 1, - ), - ) + NoteTypeFieldEditorState.Action.None return@update oldValue.copy(fields = fields.toList(), action = action) } val operation = NoteTypeFieldOperation.Reposition(oldPosition, newPosition) @@ -266,7 +255,6 @@ class NoteTypeFieldEditorViewModel( Timber.d("changeSort to $position") val fields = _state.value.fields val oldPosition = fields.indexOfFirst { it.isOrder } - val name = fields[position].name if (oldPosition == position) { val action = NoteTypeFieldEditorState.Action.None _state.value = _state.value.copy(action = action) @@ -275,7 +263,7 @@ class NoteTypeFieldEditorViewModel( _state.update { oldValue -> val list = fields.toMutableList() list.temporaryChangeSort(position) - val action = NoteTypeFieldEditorState.Action.Undoable(R.string.model_field_editor_sort_field_success_result, arrayListOf(name)) + val action = NoteTypeFieldEditorState.Action.None return@update oldValue.copy(fields = list.toList(), action = action) } val operation = NoteTypeFieldOperation.ChangeSort(oldPosition, position) @@ -306,18 +294,9 @@ class NoteTypeFieldEditorViewModel( return } _state.update { oldValue -> - val name = oldValue.fields[position].name val fields = oldValue.fields.toMutableList() fields.temporarySetLanguageHint(position, locale) - val action = - if (locale != null) { - NoteTypeFieldEditorState.Action.Undoable( - R.string.model_field_editor_language_hint_success_result, - listOf(locale.displayName, name), - ) - } else { - NoteTypeFieldEditorState.Action.Undoable(R.string.model_field_editor_language_hint_cleared_success_result, listOf(name)) - } + val action = NoteTypeFieldEditorState.Action.None return@update oldValue.copy(fields = fields.toList(), action = action) } val operation = NoteTypeFieldOperation.LanguageHint(position, oldLocale, locale) @@ -328,21 +307,6 @@ class NoteTypeFieldEditorViewModel( } } - /** - * Refreshes the uuid of the field at the given position - * - * This is used to force a UI update for a specific item even when its data - * hasn't logically changed. - * @param position the position of the field - */ - fun refreshAt(position: Int) { - _state.update { oldValue -> - val fields = oldValue.fields.toMutableList() - fields.temporaryUpdateUuid(position) - return@update oldValue.copy(fields = fields.toList()) - } - } - /** * Obtains the field list from [Collection] and refresh the state */ diff --git a/AnkiDroid/src/main/res/values-af/17-model-manager.xml b/AnkiDroid/src/main/res/values-af/17-model-manager.xml index ef6d35e0b67d..9d32373d43d6 100644 --- a/AnkiDroid/src/main/res/values-af/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-af/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-am/17-model-manager.xml b/AnkiDroid/src/main/res/values-am/17-model-manager.xml index c550609bd88f..7e814275e59e 100644 --- a/AnkiDroid/src/main/res/values-am/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-am/17-model-manager.xml @@ -50,7 +50,6 @@ ማስታወሻ አይነቱን እንደገና ይሰይሙ Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ar/17-model-manager.xml b/AnkiDroid/src/main/res/values-ar/17-model-manager.xml index 8dc6fb55f08d..c2f8a97c9056 100644 --- a/AnkiDroid/src/main/res/values-ar/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ar/17-model-manager.xml @@ -58,7 +58,6 @@ تغيير اسم نوع الملحوظة تغيير اسم نوع البطاقة - تعيين تلميح اللغة إلى %s إضافة حقل حذف الحقل @@ -75,7 +74,6 @@ Basic, Basic (and reversed card), Cloze --> أواثق من حذف أنواع الملحوظات التالية؟\n %s - ءأنت واثق أنك ترغب بحذف هذا الحقل؟ إضافة: %1$s diff --git a/AnkiDroid/src/main/res/values-az/17-model-manager.xml b/AnkiDroid/src/main/res/values-az/17-model-manager.xml index b2ee5818b063..eb5ad12aefbb 100644 --- a/AnkiDroid/src/main/res/values-az/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-az/17-model-manager.xml @@ -50,7 +50,6 @@ Qeyd növünü yenidən adlandır Kart növünü yenidən adlandır - Set language hint to %s Sahə əlavə et Sahəni sil @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Bu sahəni silmək istədiyinizə əminsiniz? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-be/17-model-manager.xml b/AnkiDroid/src/main/res/values-be/17-model-manager.xml index 3428bdd7a1d1..fe47687b0d78 100644 --- a/AnkiDroid/src/main/res/values-be/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-be/17-model-manager.xml @@ -54,7 +54,6 @@ Перайменаваць тып нататкі Перайменаваць тып карткі - Прызначыць %s у якасці мовы падказак Дадаць поле Выдаліць поле @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Выдаліць тыпы нататак ніжэй?\n %s - Вы сапраўды хочаце выдаліць гэта поле? Дадаць: %1$s diff --git a/AnkiDroid/src/main/res/values-bg/17-model-manager.xml b/AnkiDroid/src/main/res/values-bg/17-model-manager.xml index f1412270bed2..3aa60ae5b2f7 100644 --- a/AnkiDroid/src/main/res/values-bg/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-bg/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Добавяне на поле Изтриване на поле @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Сигурни ли сте, че желаете изтриване на модела? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-bn/17-model-manager.xml b/AnkiDroid/src/main/res/values-bn/17-model-manager.xml index 7828cd10f604..a8fe1ed5f17a 100644 --- a/AnkiDroid/src/main/res/values-bn/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-bn/17-model-manager.xml @@ -50,7 +50,6 @@ নোট টাইপ নাম পরিবর্তন করুন Rename card type - ভাষার ইঙ্গিতটি %s এ সেট করুন ক্ষেত্র যোগ করুন ক্ষেত্রটি মুছুন @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - আপনি এই ক্ষেত্র মুছে ফেলার বিষয়ে নিশ্চিত? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ca/17-model-manager.xml b/AnkiDroid/src/main/res/values-ca/17-model-manager.xml index b8ceade32a4b..926dfca155b9 100644 --- a/AnkiDroid/src/main/res/values-ca/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ca/17-model-manager.xml @@ -50,7 +50,6 @@ Canvieu el nom del tipus de nota Rename card type - Estableix consell d\'idioma a %s Afegeix el camp Suprimeix el camp @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Esteu segur que voleu suprimir aquest camp? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ckb/17-model-manager.xml b/AnkiDroid/src/main/res/values-ckb/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-ckb/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ckb/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-cs/17-model-manager.xml b/AnkiDroid/src/main/res/values-cs/17-model-manager.xml index 59103392bbdd..23cd96700fba 100644 --- a/AnkiDroid/src/main/res/values-cs/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-cs/17-model-manager.xml @@ -54,7 +54,6 @@ Přejmenovat typ poznámky Přejmenovat typ karty - Jazyk nápovědy nastaven na %s Přidat pole Odstranit pole @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Odstranit typy poznámek níže?\n %s - Opravdu si přejete odstranit toto pole? Přidat: %1$s diff --git a/AnkiDroid/src/main/res/values-da/17-model-manager.xml b/AnkiDroid/src/main/res/values-da/17-model-manager.xml index ce0ac5a11bd9..47923768bd17 100644 --- a/AnkiDroid/src/main/res/values-da/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-da/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-de/17-model-manager.xml b/AnkiDroid/src/main/res/values-de/17-model-manager.xml index 99f176d3e63e..1920789028a0 100644 --- a/AnkiDroid/src/main/res/values-de/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-de/17-model-manager.xml @@ -50,7 +50,6 @@ Notiztyp umbenennen Kartentyp umbenennen - Sprachhinweis auf %s festlegen Feld hinzufügen Feld löschen @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Möchten Sie dieses Feld wirklich löschen? Hinzufügen: %1$s diff --git a/AnkiDroid/src/main/res/values-el/17-model-manager.xml b/AnkiDroid/src/main/res/values-el/17-model-manager.xml index be40b8254e10..ce52f1a13e44 100644 --- a/AnkiDroid/src/main/res/values-el/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-el/17-model-manager.xml @@ -50,7 +50,6 @@ Μετονομασία τύπου σημείωσης Μετονομασία τύπου κάρτας - Ορισμός γλώσσας υποδείξεων σε %s Προσθήκη πεδίου Διαγραφή πεδίου @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Θέλετε σίγουρα να διαγράψετε αυτό το πεδίο; Προσθήκη: %1$s diff --git a/AnkiDroid/src/main/res/values-eo/17-model-manager.xml b/AnkiDroid/src/main/res/values-eo/17-model-manager.xml index de7d9b4f1502..ac47b468a6ed 100644 --- a/AnkiDroid/src/main/res/values-eo/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-eo/17-model-manager.xml @@ -50,7 +50,6 @@ Alinomi nototipon Alinomi kartotipon - Agordi lingvon de sugestoj al %s Aldoni kampon Forigi kampon @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Ĉu forigi nototipojn listigitajn sube?\n%s - Ĉu vi efektive volas forigi tiun kampon? Aldoni: %1$s diff --git a/AnkiDroid/src/main/res/values-es-rAR/17-model-manager.xml b/AnkiDroid/src/main/res/values-es-rAR/17-model-manager.xml index a8ac1a7c8ecf..396283feedf6 100644 --- a/AnkiDroid/src/main/res/values-es-rAR/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-es-rAR/17-model-manager.xml @@ -50,7 +50,6 @@ Renombrar tipo de nota Renombrar tipo de tarjeta - Establecer pista de idioma a %s Añadir campo Eliminar campo @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> ¿Eliminar los tipos de notas a continuación?\n %s - ¿Seguro que deseas eliminar este campo? Añadir: %1$s diff --git a/AnkiDroid/src/main/res/values-es-rES/17-model-manager.xml b/AnkiDroid/src/main/res/values-es-rES/17-model-manager.xml index d5f2f00771ef..0b4ab56642b0 100644 --- a/AnkiDroid/src/main/res/values-es-rES/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-es-rES/17-model-manager.xml @@ -50,7 +50,6 @@ Renombrar tipo de nota Renombrar tipo de tarjeta - Establecer pista de idioma a %s Añadir campo Borrar campo @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> ¿Eliminar los tipos de notas a continuación?\n %s - ¿Seguro que deseas eliminar este campo? Añadir: %1$s diff --git a/AnkiDroid/src/main/res/values-et/17-model-manager.xml b/AnkiDroid/src/main/res/values-et/17-model-manager.xml index fc1fbc5b04b9..190c59112d3c 100644 --- a/AnkiDroid/src/main/res/values-et/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-et/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Lisa väli Kustuta väli @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Kas olete kindel, et soovite kustutada selle välja? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-eu/17-model-manager.xml b/AnkiDroid/src/main/res/values-eu/17-model-manager.xml index abb4fd63c916..20dad4a95326 100644 --- a/AnkiDroid/src/main/res/values-eu/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-eu/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Gehitu eremua Ezabatu eremua @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Ziur zaude eremu hau ezabatu nahi izateaz? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-fa/17-model-manager.xml b/AnkiDroid/src/main/res/values-fa/17-model-manager.xml index aad2a7874ec1..a77deba11ab1 100644 --- a/AnkiDroid/src/main/res/values-fa/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fa/17-model-manager.xml @@ -50,7 +50,6 @@ تغییر نام نوع یادداشت Rename card type - تنظیم زبان راهنمایی به %s افزودن فیلد حذف فیلد @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> انواع یادداشت زیر را حذف کنید؟ %s/n - از حذف این فیلد مطمئن هستید؟ اضافه کردن: %1$s diff --git a/AnkiDroid/src/main/res/values-fi/17-model-manager.xml b/AnkiDroid/src/main/res/values-fi/17-model-manager.xml index d20b90f6424d..9f85abb13957 100644 --- a/AnkiDroid/src/main/res/values-fi/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fi/17-model-manager.xml @@ -50,7 +50,6 @@ Nimeä muistiinpanotyyppi uudelleen Uudelleennimeä korttityyppi - Aseta kielen vihjeeksi %s Lisää kenttä Poista kenttä @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Poistetaanko seuraavat muistiinpanotyypit?\n %s - Oletko varma, että haluat poistaa tämän kentän? Lisää: %1$s diff --git a/AnkiDroid/src/main/res/values-fil/17-model-manager.xml b/AnkiDroid/src/main/res/values-fil/17-model-manager.xml index 9c12b2d04ebd..f0c40bde8455 100644 --- a/AnkiDroid/src/main/res/values-fil/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fil/17-model-manager.xml @@ -50,7 +50,6 @@ Palitan ang pangalan ng uri ng tala. Rename card type - I-set ang hint sa wika sa %s Magdagdag ng field Tanggalin ang patlang @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Sigurado ka bang nais mong tanggalin ang field na ito? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-fr/17-model-manager.xml b/AnkiDroid/src/main/res/values-fr/17-model-manager.xml index 0f0a342f23bc..8c09006b31b2 100644 --- a/AnkiDroid/src/main/res/values-fr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fr/17-model-manager.xml @@ -50,7 +50,6 @@ Renommer le type de note Renommer le type de carte - Suggestion de langue définie sur %s Ajouter un champ Supprimer le champ @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Supprimer les types de note ci-dessous ?\n %s - Voulez-vous vraiment supprimer ce champ ? Ajouter : %1$s diff --git a/AnkiDroid/src/main/res/values-fy/17-model-manager.xml b/AnkiDroid/src/main/res/values-fy/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-fy/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-fy/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ga/17-model-manager.xml b/AnkiDroid/src/main/res/values-ga/17-model-manager.xml index 8fd16e1f6458..262d86162070 100644 --- a/AnkiDroid/src/main/res/values-ga/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ga/17-model-manager.xml @@ -56,7 +56,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -73,7 +72,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-gl/17-model-manager.xml b/AnkiDroid/src/main/res/values-gl/17-model-manager.xml index 6f0b50b30073..70b47cac4344 100644 --- a/AnkiDroid/src/main/res/values-gl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-gl/17-model-manager.xml @@ -50,7 +50,6 @@ Renomear tipo de nota Renomear tipo de tarxeta - Estableceuse a pista do idioma a %s Engadir campo Eliminar campo @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Queres borrar os tipos de notas de abaixo?\n%s - Tes a certeza de querer eliminar este campo? Engadir: %1$s diff --git a/AnkiDroid/src/main/res/values-got/17-model-manager.xml b/AnkiDroid/src/main/res/values-got/17-model-manager.xml index b89097969d4d..ec10d49f2777 100644 --- a/AnkiDroid/src/main/res/values-got/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-got/17-model-manager.xml @@ -50,7 +50,6 @@ 𐌼𐌴𐌻𐌴𐌹𐌽𐌰𐌹𐍃 𐌺𐌹𐌽𐌸 𐌰𐍆𐍄𐍂𐌰𐌷𐌰𐌹𐍄𐌰𐌽 𐌺𐌹𐌽𐌸 𐌺𐌰𐍂𐍄𐍉𐍃 𐌰𐍆𐍄𐍂𐌰𐌷𐌰𐌹𐍄𐌰𐌽 - 𐌷𐌹𐌻𐍀𐌰 𐍂𐌰𐌶𐌳𐍉𐍃 𐌻𐌰𐌲𐌾𐌰𐌽 𐌳𐌿 %s 𐍅𐌰𐌹𐍂𐌸 𐌱𐌹𐌰𐌿𐌺𐌰𐌽 𐍅𐌰𐌹𐍂𐌸𐌰 𐌿𐍃𐌵𐌹𐍃𐍄𐌾𐌰𐌽 @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - 𐌱𐌹 𐍃𐌿𐌽𐌾𐌰𐌹 𐍅𐌹𐌻𐌴𐌹𐌶𐌿 𐌸𐌰𐌼𐌼𐌰 𐍅𐌰𐌹𐍂𐌸𐌰 𐌿𐍃𐌵𐌹𐍃𐍄𐌾𐌰𐌽? 𐌱𐌹𐌰𐌿𐌺𐌰𐌽: %1$s diff --git a/AnkiDroid/src/main/res/values-gu/17-model-manager.xml b/AnkiDroid/src/main/res/values-gu/17-model-manager.xml index c5800b5f8a6d..a8404c8de09f 100644 --- a/AnkiDroid/src/main/res/values-gu/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-gu/17-model-manager.xml @@ -50,7 +50,6 @@ નોંધ પ્રકારનું નામ બદલો Rename card type - ભાષા સંકેતને %s પર સેટ કરો ક્ષેત્ર ઉમેરો ક્ષેત્ર કાઢી નાખો @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - શું તમે ખરેખર આ ફીલ્ડ કાઢી નાખવા માંગો છો? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-heb/17-model-manager.xml b/AnkiDroid/src/main/res/values-heb/17-model-manager.xml index c3301b4102e2..9350e56a4130 100644 --- a/AnkiDroid/src/main/res/values-heb/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-heb/17-model-manager.xml @@ -54,7 +54,6 @@ שינוי שם לסוג הרשומה שינוי שם סוג הכרטיס - הגדר רמז שפה ל-%s הוספת שדה מחיקת שדה @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - למחוק את השדה הזה? הוספה: %1$s diff --git a/AnkiDroid/src/main/res/values-hi/17-model-manager.xml b/AnkiDroid/src/main/res/values-hi/17-model-manager.xml index 00b63de7b6e1..8a59f7d8b694 100644 --- a/AnkiDroid/src/main/res/values-hi/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-hi/17-model-manager.xml @@ -50,7 +50,6 @@ नोट के प्रकार का नाम बदलें Rename card type - भाषा संकेत के रूप में %s सेट करें फ़ील्ड जोड़ें फ़ील्ड मिटाएँ @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - क्या आप सचमुच इस फ़ील्ड को मिटाना चाहते हैं? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-hr/17-model-manager.xml b/AnkiDroid/src/main/res/values-hr/17-model-manager.xml index 3fd1d9ff59af..e9bcf64f6acf 100644 --- a/AnkiDroid/src/main/res/values-hr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-hr/17-model-manager.xml @@ -52,7 +52,6 @@ Preimenuj tip bilješke Preimenuj tip kartice - Postavi jezik polja na %s Dodaj polje Izbriši polje @@ -69,7 +68,6 @@ Basic, Basic (and reversed card), Cloze --> Izbrisati niže navedene tipove bilješki?\n %s - Jeste li sigurni da želite izbrisati ovo polje? Dodaj: %1$s diff --git a/AnkiDroid/src/main/res/values-hu/17-model-manager.xml b/AnkiDroid/src/main/res/values-hu/17-model-manager.xml index 8b5b6e23ee6d..29e9b169b2dd 100644 --- a/AnkiDroid/src/main/res/values-hu/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-hu/17-model-manager.xml @@ -50,7 +50,6 @@ Jegyzettípus átnevezése Rename card type - Nyelvi javaslat beállítás %s Mező hozzáadása Mező törlése @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Biztosan törölni akarja ezt a mezőt? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-hy/17-model-manager.xml b/AnkiDroid/src/main/res/values-hy/17-model-manager.xml index 3ed7656621af..52707baf851a 100644 --- a/AnkiDroid/src/main/res/values-hy/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-hy/17-model-manager.xml @@ -50,7 +50,6 @@ Վերանվանել տեսակը Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ind/17-model-manager.xml b/AnkiDroid/src/main/res/values-ind/17-model-manager.xml index 05174d5044fb..ffd24f300e3a 100644 --- a/AnkiDroid/src/main/res/values-ind/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ind/17-model-manager.xml @@ -48,7 +48,6 @@ Ubah nama tipe catatan Rename card type - Atur petunjuk bahasa menjadi %s Tambah kolom Hapus kolom @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Yakin ingin menghapus kolom ini? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-it/17-model-manager.xml b/AnkiDroid/src/main/res/values-it/17-model-manager.xml index 981d7e2c4356..a5cdcb29e172 100644 --- a/AnkiDroid/src/main/res/values-it/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-it/17-model-manager.xml @@ -50,7 +50,6 @@ Rinoma il tipo di nota Rinomina il tipo di carta - Imposta suggerimento lingua a %s Aggiungi campo Elimina campo @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Sei sicuro di voler eliminare questo campo? Aggiungi: %1$s diff --git a/AnkiDroid/src/main/res/values-iw/17-model-manager.xml b/AnkiDroid/src/main/res/values-iw/17-model-manager.xml index c3301b4102e2..9350e56a4130 100644 --- a/AnkiDroid/src/main/res/values-iw/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-iw/17-model-manager.xml @@ -54,7 +54,6 @@ שינוי שם לסוג הרשומה שינוי שם סוג הכרטיס - הגדר רמז שפה ל-%s הוספת שדה מחיקת שדה @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - למחוק את השדה הזה? הוספה: %1$s diff --git a/AnkiDroid/src/main/res/values-ja/17-model-manager.xml b/AnkiDroid/src/main/res/values-ja/17-model-manager.xml index 4679d2140c8a..125af078ccc6 100644 --- a/AnkiDroid/src/main/res/values-ja/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ja/17-model-manager.xml @@ -48,7 +48,6 @@ ノートタイプの名前を変更 カードタイプの名前を変更 - %s をキーボード言語に設定しました フィールドを追加 削除 @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> 下記のノートタイプを削除してもよろしいですか?\n%s - このフィールドを削除します。本当によろしいですか? 追加: %1$s diff --git a/AnkiDroid/src/main/res/values-ka/17-model-manager.xml b/AnkiDroid/src/main/res/values-ka/17-model-manager.xml index 2eccd89316f1..9f06ad8ed3ee 100644 --- a/AnkiDroid/src/main/res/values-ka/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ka/17-model-manager.xml @@ -50,7 +50,6 @@ შენიშვნის ტიპის სახელის შეცვლა Rename card type - %s-თან ენის მინიშნების დაყენება ველის დამატება ველის წაშლა @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - ნამდვილად გსურთ ამ ველის წაშლა? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-kk/17-model-manager.xml b/AnkiDroid/src/main/res/values-kk/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-kk/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-kk/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-km/17-model-manager.xml b/AnkiDroid/src/main/res/values-km/17-model-manager.xml index c3968893d949..81e13cd64c18 100644 --- a/AnkiDroid/src/main/res/values-km/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-km/17-model-manager.xml @@ -48,7 +48,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-kn/17-model-manager.xml b/AnkiDroid/src/main/res/values-kn/17-model-manager.xml index 93d1f62e1fe4..2f15afe3b7c5 100644 --- a/AnkiDroid/src/main/res/values-kn/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-kn/17-model-manager.xml @@ -50,7 +50,6 @@ ಟಿಪ್ಪಣಿ ಪ್ರಕಾರವನ್ನು ಮರುಹೆಸರಿಸಿ Rename card type - ಭಾಷೆಯ ಸುಳಿವನ್ನು %s ಗೆ ಹೊಂದಿಸಿ ಕ್ಷೇತ್ರವನ್ನು ಸೇರಿಸಿ ಕ್ಷೇತ್ರವನ್ನು ಅಳಿಸಿ @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - ಈ ಕ್ಷೇತ್ರವನ್ನು ಅಳಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ko/17-model-manager.xml b/AnkiDroid/src/main/res/values-ko/17-model-manager.xml index 80b696941c0b..41f37f0e8e60 100644 --- a/AnkiDroid/src/main/res/values-ko/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ko/17-model-manager.xml @@ -48,7 +48,6 @@ 노트 유형 이름 바꾸기 카드 유형 이름 바꾸기 - 키보드 언어 힌트를 %s로 설정하기 필드 추가하기 필드 삭제하기 @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - 이 필드를 삭제하시겠습니까? 추가: %1$s diff --git a/AnkiDroid/src/main/res/values-ku/17-model-manager.xml b/AnkiDroid/src/main/res/values-ku/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-ku/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ku/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ky/17-model-manager.xml b/AnkiDroid/src/main/res/values-ky/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-ky/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ky/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-lt/17-model-manager.xml b/AnkiDroid/src/main/res/values-lt/17-model-manager.xml index a45c1891cdc6..3a6de13bb42f 100644 --- a/AnkiDroid/src/main/res/values-lt/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-lt/17-model-manager.xml @@ -54,7 +54,6 @@ Pervadinti užrašo tipą Rename card type - Nustatyti kalbos užuominą į %s Pridėti laukelį Ištrinti laukelį @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Ar tikrai norite ištrinti šį laukelį? Pridėti: %1$s diff --git a/AnkiDroid/src/main/res/values-lv/17-model-manager.xml b/AnkiDroid/src/main/res/values-lv/17-model-manager.xml index 320831d8cec1..a2d85af7fd4b 100644 --- a/AnkiDroid/src/main/res/values-lv/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-lv/17-model-manager.xml @@ -52,7 +52,6 @@ Pārdēvēt piezīmju veidu Rename card type - Iestatīt valodas norādi %s Pievienot lauku Izdzēst lauku @@ -69,7 +68,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Vai tiešām izdzēst šo lauku? Pievienot: %1$s diff --git a/AnkiDroid/src/main/res/values-mk/17-model-manager.xml b/AnkiDroid/src/main/res/values-mk/17-model-manager.xml index 42d8c8db1b64..0b50891cd3cd 100644 --- a/AnkiDroid/src/main/res/values-mk/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-mk/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Додај поле Избриши поле @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Дали сте сигурни дека сакате да го избришете ова поле? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ml/17-model-manager.xml b/AnkiDroid/src/main/res/values-ml/17-model-manager.xml index 53dd1f095b47..758ef1e3f997 100644 --- a/AnkiDroid/src/main/res/values-ml/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ml/17-model-manager.xml @@ -50,7 +50,6 @@ നോട്ട് തരം പുനർനാമകരണം ചെയ്യുക Rename card type - ഭാഷാ സൂചന %s ആയി സജ്ജമാക്കുക ഫീൽഡ് ചേർക്കുക ഫീൽഡ് ഇല്ലാതാക്കുക @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - ഈ ഫീൽഡ് ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-mn/17-model-manager.xml b/AnkiDroid/src/main/res/values-mn/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-mn/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-mn/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-mr/17-model-manager.xml b/AnkiDroid/src/main/res/values-mr/17-model-manager.xml index 9ba62c48456a..198efa4dd698 100644 --- a/AnkiDroid/src/main/res/values-mr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-mr/17-model-manager.xml @@ -50,7 +50,6 @@ टीप प्रकार पुनर्नामित करा Rename card type - भाषेचा संकेत%s वर सेट करा फील्ड जोडा फील्ड हटवा @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - आपली खात्री आहे की आपण हे फील्ड हटवू इच्छिता? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ms/17-model-manager.xml b/AnkiDroid/src/main/res/values-ms/17-model-manager.xml index 06091b4add19..4537a4c337fe 100644 --- a/AnkiDroid/src/main/res/values-ms/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ms/17-model-manager.xml @@ -48,7 +48,6 @@ Nama semula jenis nota Rename card type - Tetapkan petunjuk bahasa kepada %s Tambah Medan Hapuskan medan @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Anda pasti ingin menghapuskan medan ini? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-my/17-model-manager.xml b/AnkiDroid/src/main/res/values-my/17-model-manager.xml index c3968893d949..81e13cd64c18 100644 --- a/AnkiDroid/src/main/res/values-my/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-my/17-model-manager.xml @@ -48,7 +48,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-nl/17-model-manager.xml b/AnkiDroid/src/main/res/values-nl/17-model-manager.xml index 239b6a5d2d9d..78ad6f63b0be 100644 --- a/AnkiDroid/src/main/res/values-nl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-nl/17-model-manager.xml @@ -50,7 +50,6 @@ Memotype hernoemen Rename card type - Taaltip op %s instellen Veld toevoegen Veld verwijderen @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Weet u zeker dat u dit veld wilt verwijderen? Toevoegen: %1$s diff --git a/AnkiDroid/src/main/res/values-nn/17-model-manager.xml b/AnkiDroid/src/main/res/values-nn/17-model-manager.xml index 40767483afe0..841a46070ab4 100644 --- a/AnkiDroid/src/main/res/values-nn/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-nn/17-model-manager.xml @@ -50,7 +50,6 @@ Gi nytt navn til notattype Rename card type - Set language hint to %s Legg til felt Slett felt @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Er du sikker på at du vil sletta dette feltet? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-no/17-model-manager.xml b/AnkiDroid/src/main/res/values-no/17-model-manager.xml index 74c0d3823cb0..36bcb88fb986 100644 --- a/AnkiDroid/src/main/res/values-no/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-no/17-model-manager.xml @@ -50,7 +50,6 @@ Gi nytt navn til notattype Rename card type - Set language hint to %s Legg til felt Slett felt @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Er du sikker på at du vil slette dette feltet? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-or/17-model-manager.xml b/AnkiDroid/src/main/res/values-or/17-model-manager.xml index 652a09cca9c3..edfe95f96024 100644 --- a/AnkiDroid/src/main/res/values-or/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-or/17-model-manager.xml @@ -50,7 +50,6 @@ ନୋଟ୍ ପ୍ରକାରର ନାମ ବଦଳାଇବା ପତ୍ର ପ୍ରକାରର ନାମ ବଦଳାଇବା - ଭାଷା ସୂଚକ କୁ %s ସେଟ୍ କରନ୍ତୁ କ୍ଷେତ୍ର ଯୋଡ଼ିବା କ୍ଷେତ୍ରଟି ଵିଲୋପ କରିବା @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - ଆପଣ ନିଶ୍ଚିତ କି ଆପଣ ଏହି କ୍ଷେତ୍ର ବିଲୋପ କରିବାକୁ ଚାହୁଁଛନ୍ତି? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-pa/17-model-manager.xml b/AnkiDroid/src/main/res/values-pa/17-model-manager.xml index 36fd30478efa..3fee9b911904 100644 --- a/AnkiDroid/src/main/res/values-pa/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-pa/17-model-manager.xml @@ -50,7 +50,6 @@ ਨੋਟ ਦੀ ਕਿਸਮ ਦਾ ਨਾਮ ਬਦਲੋ Rename card type - ਭਾਸ਼ਾ ਸੰਕੇਤਕ ਨੂੰ %s \'ਤੇ ਸੈੱਟ ਕਰੋ ਖੇਤਰ ਸ਼ਾਮਲ ਕਰੋ ਖੇਤਰ ਮਿਟਾਓ @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - ਕੀ ਤੁਸੀਂ ਯਕੀਨੀ ਤੌਰ \'ਤੇ ਇਸ ਖੇਤਰ ਨੂੰ ਮਿਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-pl/17-model-manager.xml b/AnkiDroid/src/main/res/values-pl/17-model-manager.xml index 84062c23a54e..1fff0eae2c49 100644 --- a/AnkiDroid/src/main/res/values-pl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-pl/17-model-manager.xml @@ -54,7 +54,6 @@ Zmień nazwę typu notatki Zmień nazwę typu karty - Ustaw podpowiedź językową na %s Dodaj pole Usuń pole @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Usunąć poniższe typy notatek?\n %s - Czy na pewno chcesz usunąć to pole? Dodaj: %1$s diff --git a/AnkiDroid/src/main/res/values-pt-rBR/17-model-manager.xml b/AnkiDroid/src/main/res/values-pt-rBR/17-model-manager.xml index e88e6fbcf0cb..8544c99a1da2 100644 --- a/AnkiDroid/src/main/res/values-pt-rBR/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-pt-rBR/17-model-manager.xml @@ -50,7 +50,6 @@ Renomear categorias de nota Nomear tipo de carta - Definir idioma da dica para %s Adicionar campo Excluir campo @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Apagar os tipos de notas abaixo?\n %s - Tem certeza que deseja excluir este campo? Adicionar: %1$s diff --git a/AnkiDroid/src/main/res/values-pt-rPT/17-model-manager.xml b/AnkiDroid/src/main/res/values-pt-rPT/17-model-manager.xml index 17c09263965d..46df6ca1ac15 100644 --- a/AnkiDroid/src/main/res/values-pt-rPT/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-pt-rPT/17-model-manager.xml @@ -50,7 +50,6 @@ Renomear tipo de nota Renomeie o tipo de ficha - Definir a língua associada como %s Adicionar campo Eliminar campo @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Tem a certeza que pretende eliminar este campo? Adicionar: %1$s diff --git a/AnkiDroid/src/main/res/values-ro/17-model-manager.xml b/AnkiDroid/src/main/res/values-ro/17-model-manager.xml index 8bb865abe8ad..a5f90d3c1a5d 100644 --- a/AnkiDroid/src/main/res/values-ro/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ro/17-model-manager.xml @@ -52,7 +52,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -69,7 +68,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ru/17-model-manager.xml b/AnkiDroid/src/main/res/values-ru/17-model-manager.xml index d26d677fa90a..3d20ad2b8c78 100644 --- a/AnkiDroid/src/main/res/values-ru/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ru/17-model-manager.xml @@ -54,7 +54,6 @@ Переименовать тип записи Переименовать тип карточки - Установить %s как язык подсказок Добавить поле Удалить поле @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Удалить типы заметок ниже?\n %s - Удалить это поле? Добавить: %1$s diff --git a/AnkiDroid/src/main/res/values-sat/17-model-manager.xml b/AnkiDroid/src/main/res/values-sat/17-model-manager.xml index 06a64a06da6c..cfdbd93f08ae 100644 --- a/AnkiDroid/src/main/res/values-sat/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sat/17-model-manager.xml @@ -50,7 +50,6 @@ ᱠᱷᱟᱴᱚ ᱵᱤᱪᱟᱹᱨ ᱯᱨᱚᱠᱟᱨ ᱫᱩᱦᱰᱟ ᱧᱩᱛᱩᱢ Rename card type - %s ᱛᱮ ᱯᱟᱹᱨᱥᱤ ᱦᱤᱸᱴ ᱥᱮᱴ ᱢᱮ ᱡᱟᱭᱜᱟ ᱥᱮᱞᱮᱫ ᱢᱮ ᱡᱟᱭᱜᱟ ᱜᱮᱫ ᱜᱤᱰᱤ @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - ᱥᱭᱚᱨ ᱱᱚᱶᱟ ᱡᱟᱭᱜᱟ ᱜᱮᱫ ᱜᱤᱲᱤ ᱥᱟᱱᱟᱢ ᱠᱟᱱᱟ? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-sc/17-model-manager.xml b/AnkiDroid/src/main/res/values-sc/17-model-manager.xml index f71c3eefcf32..12a1253efdd0 100644 --- a/AnkiDroid/src/main/res/values-sc/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sc/17-model-manager.xml @@ -50,7 +50,6 @@ Càmbia nùmene a sa casta de nota Rename card type - Imposta s\'impòsitu de limba a %s Annanghe unu campu Iscantzella su campu @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Ses seguru de chèrrere iscantzellare custu campu? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-sk/17-model-manager.xml b/AnkiDroid/src/main/res/values-sk/17-model-manager.xml index 441ffa725450..1480124a92b2 100644 --- a/AnkiDroid/src/main/res/values-sk/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sk/17-model-manager.xml @@ -54,7 +54,6 @@ Premenovať typ poznámok Rename card type - Nastaviť jazykový tip na %s Pridať pole Vymazať pole @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Skutočne si prajete odstrániť toto pole? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-sl/17-model-manager.xml b/AnkiDroid/src/main/res/values-sl/17-model-manager.xml index bc428fdb8993..ae2c6f8a05dc 100644 --- a/AnkiDroid/src/main/res/values-sl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sl/17-model-manager.xml @@ -54,7 +54,6 @@ Rename note type Rename card type - Set language hint to %s Dodaj polje Izbriši polje @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Ali res želite izbrisati to polje? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-sq/17-model-manager.xml b/AnkiDroid/src/main/res/values-sq/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-sq/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sq/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-sr/17-model-manager.xml b/AnkiDroid/src/main/res/values-sr/17-model-manager.xml index 77a6c9249693..65c3094fa472 100644 --- a/AnkiDroid/src/main/res/values-sr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sr/17-model-manager.xml @@ -52,7 +52,6 @@ Rename note type Rename card type - Set language hint to %s Додај поље Избриши поље @@ -69,7 +68,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Да ли заиста желите да избришете овај модел? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-sv/17-model-manager.xml b/AnkiDroid/src/main/res/values-sv/17-model-manager.xml index d4cef06ce957..11b2bf3f2a61 100644 --- a/AnkiDroid/src/main/res/values-sv/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-sv/17-model-manager.xml @@ -50,7 +50,6 @@ Byt namn på nottyp Rename card type - Ställ in språktips till %s Lägg till fält Ta bort fält @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Är du säker på att du vill ta bort detta fält? Lägg till: %1$s diff --git a/AnkiDroid/src/main/res/values-ta/17-model-manager.xml b/AnkiDroid/src/main/res/values-ta/17-model-manager.xml index c92d3f36b179..9efed6af6316 100644 --- a/AnkiDroid/src/main/res/values-ta/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ta/17-model-manager.xml @@ -50,7 +50,6 @@ குறிப்பு வகையை மறுபெயரிடவும் Rename card type - மொழி குறிப்பை %s ஆக அமைக்கவும் ஒரு புலத்தைச் சேர்க்கவும் புலத்தை நீக்கு @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - இந்தப் புலத்தை நிச்சயமாக நீக்க விரும்புகிறீர்களா? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-te/17-model-manager.xml b/AnkiDroid/src/main/res/values-te/17-model-manager.xml index 7755c49de6c1..2da70c5dd409 100644 --- a/AnkiDroid/src/main/res/values-te/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-te/17-model-manager.xml @@ -50,7 +50,6 @@ గమనిక రకం పేరు మార్చండి Rename card type - భాష సూచనను %sకి సెట్ చేయండి ఫీల్డ్ను జోడించండి ఫీల్డ్ను తొలగించు @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - మీరు ఖచ్చితంగా ఈ ఫీల్డ్ని తొలగించాలనుకుంటున్నారా? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-tgl/17-model-manager.xml b/AnkiDroid/src/main/res/values-tgl/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-tgl/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-tgl/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-th/17-model-manager.xml b/AnkiDroid/src/main/res/values-th/17-model-manager.xml index 5e61f43b3387..1d836716f715 100644 --- a/AnkiDroid/src/main/res/values-th/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-th/17-model-manager.xml @@ -48,7 +48,6 @@ เปลี่ยนชื่อรูปแบบโน๊ต Rename card type - Set language hint to %s Add field Delete field @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ti/17-model-manager.xml b/AnkiDroid/src/main/res/values-ti/17-model-manager.xml index c3c60b3f6ee8..3f000750f945 100644 --- a/AnkiDroid/src/main/res/values-ti/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ti/17-model-manager.xml @@ -50,7 +50,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-tr/17-model-manager.xml b/AnkiDroid/src/main/res/values-tr/17-model-manager.xml index a6a8b09c4dd3..410157acfb9f 100644 --- a/AnkiDroid/src/main/res/values-tr/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-tr/17-model-manager.xml @@ -50,7 +50,6 @@ Not türünün adını değiştir Kart türünün ismini değiştir - Dil ipucunu %s olarak ayarla Alan ekle Alanı sil @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Aşağıdaki not türleri silinsin mi?\n %s - Bu alanı silmek istediğinize emin misiniz? Ekle: %1$s diff --git a/AnkiDroid/src/main/res/values-tt/17-model-manager.xml b/AnkiDroid/src/main/res/values-tt/17-model-manager.xml index c3968893d949..81e13cd64c18 100644 --- a/AnkiDroid/src/main/res/values-tt/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-tt/17-model-manager.xml @@ -48,7 +48,6 @@ Rename note type Rename card type - Set language hint to %s Add field Delete field @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? Add: %1$s diff --git a/AnkiDroid/src/main/res/values-ug/17-model-manager.xml b/AnkiDroid/src/main/res/values-ug/17-model-manager.xml index 7630bb243469..0b2f7753944f 100644 --- a/AnkiDroid/src/main/res/values-ug/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ug/17-model-manager.xml @@ -50,7 +50,6 @@ خاتىرە تۈرىنىڭ ئاتىنى ئۆزگەرتىدۇ كارتا تۈرىنىڭ ئاتىنى ئۆزگەرتىدۇ - تىل كۆرسەتمىسىنى %s غا تەڭشەيدۇ بۆلەك قوش بۆلەك ئۆچۈر @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> تۆۋەندىكى خاتىرە تۈرىنى ئۆچۈرەمدۇ؟\n %s - بۇ بۆلەكنى راستتىنلا ئۆچۈرەمسىز؟ قوش: %1$s diff --git a/AnkiDroid/src/main/res/values-uk/17-model-manager.xml b/AnkiDroid/src/main/res/values-uk/17-model-manager.xml index b0f0e0bbe59c..f4e2b4f5b251 100644 --- a/AnkiDroid/src/main/res/values-uk/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-uk/17-model-manager.xml @@ -54,7 +54,6 @@ Перейменувати тип запису Перейменувати тип картки - Встановити %s як мову підказок Додати поле Видалити поле @@ -71,7 +70,6 @@ Basic, Basic (and reversed card), Cloze --> Видалити типи записів нижче?\n %s - Ви впевнені, що хочете видалити це поле? Додати: %1$s diff --git a/AnkiDroid/src/main/res/values-ur/17-model-manager.xml b/AnkiDroid/src/main/res/values-ur/17-model-manager.xml index fd9f53dcc228..e7de7f99b22b 100644 --- a/AnkiDroid/src/main/res/values-ur/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-ur/17-model-manager.xml @@ -50,7 +50,6 @@ نوٹ کی قسم کا نام تبدیل کریں۔ Rename card type - زبان کے اشارے کو %s پر سیٹ کریں۔ فیلڈ شامل کریں فیلڈ کو حذف کریں۔ @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - کیا آپ واقعی اس فیلڈ کو حذف کرنا چاہتے ہیں؟ Add: %1$s diff --git a/AnkiDroid/src/main/res/values-uz/17-model-manager.xml b/AnkiDroid/src/main/res/values-uz/17-model-manager.xml index 7191d24d8d46..8ef25ea9b509 100644 --- a/AnkiDroid/src/main/res/values-uz/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-uz/17-model-manager.xml @@ -50,7 +50,6 @@ Qayd turi nomini oʻzgartirish Karta turi nomini oʻzgartirish - Til tavsiyasi %sga sozlandi Maydon qoʻshish Maydonni oʻchirib tashlash @@ -67,7 +66,6 @@ Basic, Basic (and reversed card), Cloze --> Quyidagi qayd turlari oʻchirilsinmi?\n %s - Rostdan ham bu maydonni oʻchirib tashlamoqchimisiz? Qoʻsh: %1$s diff --git a/AnkiDroid/src/main/res/values-vi/17-model-manager.xml b/AnkiDroid/src/main/res/values-vi/17-model-manager.xml index 91deb7bf19c5..5f313a40b377 100644 --- a/AnkiDroid/src/main/res/values-vi/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-vi/17-model-manager.xml @@ -48,7 +48,6 @@ Đổi tên loại ghi chú Đổi tên loại thẻ - Đặt gợi ý ngôn ngữ thành %s Thêm trường Xóa trường @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Bạn có chắc chắn muốn xóa trường này không? Thêm: %1$s diff --git a/AnkiDroid/src/main/res/values-zh-rCN/17-model-manager.xml b/AnkiDroid/src/main/res/values-zh-rCN/17-model-manager.xml index be0b3def5933..f31327a4628a 100644 --- a/AnkiDroid/src/main/res/values-zh-rCN/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-zh-rCN/17-model-manager.xml @@ -48,7 +48,6 @@ 重命名笔记模板 重命名卡牌类型 - 设置语言提示为 %s 添加字段 删除字段 @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> 删除以下笔记模板?\n %s - 是否确定删除此字段? 添加:%1$s diff --git a/AnkiDroid/src/main/res/values-zh-rTW/17-model-manager.xml b/AnkiDroid/src/main/res/values-zh-rTW/17-model-manager.xml index 8d866583292f..f61ef072b28b 100644 --- a/AnkiDroid/src/main/res/values-zh-rTW/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values-zh-rTW/17-model-manager.xml @@ -48,7 +48,6 @@ 重新命名筆記類型 重命名卡片種類 - 將語言提示設為%s 新增欄位 刪除欄位 @@ -65,7 +64,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - 你確定要刪除此欄位嗎? 添加:%1$s diff --git a/AnkiDroid/src/main/res/values/17-model-manager.xml b/AnkiDroid/src/main/res/values/17-model-manager.xml index 0d8c8bfaca92..12d90d499950 100644 --- a/AnkiDroid/src/main/res/values/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values/17-model-manager.xml @@ -37,8 +37,6 @@ Rename card type - Set language hint for ‘%2$s’ to ‘%1$s’ - Cleared language hint for ‘%1$s’ Add field Delete field @@ -47,12 +45,7 @@ Reposition field Updating fields Sort by this field - Added ‘%1$s’ Deleted ‘%1$s’ - Renamed ‘%1$s’ to ‘%2$s’ - Moved ‘%1$s’ from %2$d to %3$d - Set ‘%1$s’ as the sort field - Tap the small check icon to confirm field name Saved changes successfully Discarded changes These changes cannot be undone. Are you sure you wish to save these changes? @@ -66,7 +59,6 @@ Basic, Basic (and reversed card), Cloze --> Delete the note types below?\n %s - Are you sure you wish to delete this field? From 4d17f35114cd08b61323ae92f2f33569677f9623 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:09:34 +0900 Subject: [PATCH 15/19] feat: add real-time field name validation Extracted field name validation logic. Implemented real-time validation in the field editor. Use `ConstraintLayout` to avoid UI corruption when error/helper text is showed. Added a reset button in the text box to restore the original field name. --- .../fieldeditor/AddNewNoteTypeField.kt | 64 ++++- .../fieldeditor/NoteTypeFieldEditor.kt | 98 +++++--- .../NoteTypeFieldEditorViewModel.kt | 219 ++++++------------ .../fieldeditor/NoteTypeFieldRowData.kt | 13 +- .../main/java/com/ichi2/utils/FieldUtil.kt | 84 +++++++ .../main/res/layout/item_notetype_field.xml | 69 ++++-- .../src/main/res/values/17-model-manager.xml | 2 + 7 files changed, 341 insertions(+), 208 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/utils/FieldUtil.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt index 9eddb6bf8c6e..84a1cd645095 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/AddNewNoteTypeField.kt @@ -3,8 +3,9 @@ package com.ichi2.anki.notetype.fieldeditor import androidx.appcompat.app.AlertDialog import com.ichi2.anki.R import com.ichi2.anki.launchCatchingTask -import com.ichi2.ui.FixedEditText -import com.ichi2.utils.customView +import com.ichi2.utils.FieldUtil +import com.ichi2.utils.getInputField +import com.ichi2.utils.input import com.ichi2.utils.negativeButton import com.ichi2.utils.positiveButton import com.ichi2.utils.show @@ -12,25 +13,68 @@ import com.ichi2.utils.title class AddNewNoteTypeField( private val activity: NoteTypeFieldEditor, + private val existingNameList: List, ) { fun showAddNewNoteTypeFieldDialog(confirm: (String) -> Unit) { - val fieldNameInput = - FixedEditText(activity).apply { - focusWithKeyboard() - isSingleLine = true - } - activity.apply { launchCatchingTask { AlertDialog .Builder(activity) .show { - customView(fieldNameInput, paddingStart = 64, paddingEnd = 64, paddingTop = 32) + setView(R.layout.dialog_generic_text_input) title(R.string.model_field_editor_add) positiveButton(R.string.menu_add) { - confirm(fieldNameInput.text.toString()) + val userInput = (it as AlertDialog).getInputField().text.toString() + confirm(userInput) } negativeButton(R.string.dialog_cancel) + }.input( + hint = getString(R.string.model_field_editor_name), + displayKeyboard = true, + allowEmpty = true, + waitForPositiveButton = false, + ) { dialog, char -> + val name = char.toString() + val result = + FieldUtil.uniqueName( + nameList = existingNameList, + newName = name, + ) + val textInputLayout = + dialog.findViewById( + R.id.dialog_text_input_layout, + ) + textInputLayout?.apply { + when (result) { + is FieldUtil.UniqueNameResult.Success -> { + helperText = + if (name != result.name) { + getString( + R.string.model_field_editor_auto_rename, + result.name, + ) + } else { + null + } + error = null + isErrorEnabled = false + dialog.positiveButton.isEnabled = true + } + + FieldUtil.UniqueNameResult.Failure.DuplicateName -> { + helperText = null + error = getString(R.string.toast_duplicate_field) + dialog.positiveButton.isEnabled = false + } + + FieldUtil.UniqueNameResult.Failure.EmptyName -> { + // Differs from the rename operation + helperText = null + error = getString(R.string.toast_empty_name) + dialog.positiveButton.isEnabled = false + } + } + } } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt index 5fdb8ebb810e..f519f925090c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditor.kt @@ -17,8 +17,6 @@ package com.ichi2.anki.notetype.fieldeditor import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher import android.view.LayoutInflater import android.view.Menu import android.view.ViewGroup @@ -26,6 +24,7 @@ import android.view.inputmethod.EditorInfo import androidx.activity.addCallback import androidx.activity.viewModels import androidx.core.os.BundleCompat +import androidx.core.widget.addTextChangedListener import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -54,6 +53,7 @@ import com.ichi2.anki.utils.ext.getIntOrNull import com.ichi2.anki.utils.ext.setFragmentResultListener import com.ichi2.anki.utils.ext.showDialogFragment import com.ichi2.anki.utils.hideKeyboard +import com.ichi2.utils.FieldUtil import com.ichi2.utils.moveCursorToEnd import dev.androidbroadcast.vbpd.viewBinding import kotlinx.coroutines.Dispatchers @@ -188,12 +188,16 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field } binding.fields.apply { - setHasFixedSize(true) layoutManager = LinearLayoutManager(this@NoteTypeFieldEditor) adapter = this@NoteTypeFieldEditor.adapter touchHelper.attachToRecyclerView(this@apply) } - binding.btnAdd.setOnClickListener { addFieldDialog() } + binding.btnAdd.setOnClickListener { + val existingNameList = + viewModel.state.value.fields + .map { it.name.savedName } + addFieldDialog(existingNameList) + } onBackPressedDispatcher.addCallback(this) { viewModel.requestDiscardChangesAndClose() } @@ -316,8 +320,8 @@ class NoteTypeFieldEditor : com.ichi2.anki.AnkiActivity(R.layout.note_type_field /** * Creates a dialog to create a field */ - private fun addFieldDialog() { - AddNewNoteTypeField(this).showAddNewNoteTypeFieldDialog { name -> + private fun addFieldDialog(existingNameList: List) { + AddNewNoteTypeField(this, existingNameList).showAddNewNoteTypeFieldDialog { name -> viewModel.add(name = name) } } @@ -432,7 +436,7 @@ private class NoteFieldAdapter( Timber.d("payload: $oldItemPosition, $newItemPosition") Timber.d("payload: $oldItem, $newItem") return when { - oldItem.name != newItem.name -> Payload.Rename + oldItem.displayName != newItem.displayName -> Payload.Rename oldItem.isOrder != newItem.isOrder -> Payload.Sort oldItem.locale != newItem.locale -> Payload.Locale else -> super.getChangePayload(oldItemPosition, newItemPosition) @@ -496,29 +500,17 @@ private class NoteFieldAdapter( } } addTextChangedListener( - object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - end: Int, - ) { - } - - override fun onTextChanged( - s: CharSequence?, - start: Int, - count: Int, - end: Int, - ) { - } - - override fun afterTextChanged(s: Editable?) { - save() - } + afterTextChanged = { + save() }, ) } + fieldEditLayout.setEndIconOnClickListener { + val position = bindingAdapterPosition + if (position == RecyclerView.NO_POSITION) return@setEndIconOnClickListener + val name = getItem(position).name.savedName + setText(name) + } } } @@ -529,9 +521,13 @@ private class NoteFieldAdapter( Timber.d("bind: $item at $bindingAdapterPosition ") binding.root.translationX = 0f binding.apply { - if (payload.contains(NoteTypeFieldDiffUtil.Payload.Rename) && item.name != fieldEdit.text?.toString().orEmpty()) { + if (payload.contains(NoteTypeFieldDiffUtil.Payload.Rename)) { Timber.d("field edittext: ${fieldEdit.text} to ${item.name} at $bindingAdapterPosition") - setText(item.name) + if (item.displayName != fieldEdit.text?.toString().orEmpty()) { + setText(item.displayName) + } + setEndIconVisible() + setError() } if (payload.contains(NoteTypeFieldDiffUtil.Payload.Sort)) { fieldSortButton.isChecked = item.isOrder @@ -551,6 +547,50 @@ private class NoteFieldAdapter( } } + fun setEndIconVisible() { + val position = bindingAdapterPosition + if (position == RecyclerView.NO_POSITION) return + val item = getItem(position) + binding.fieldEditLayout.isEndIconVisible = + item.displayName != item.name.savedName && item.displayName.isNotBlank() + } + + fun setError() { + val position = bindingAdapterPosition + if (position == RecyclerView.NO_POSITION) return + val item = getItem(position) + binding.fieldEditLayout.apply { + when (val result = item.name.valid) { + is FieldUtil.UniqueNameResult.Success -> { + if (item.displayName != result.name) { + helperText = + binding.root.resources.getString( + R.string.model_field_editor_auto_rename, + result.name, + ) + error = null + } else { + helperText = null + error = null + } + isErrorEnabled = false + } + + FieldUtil.UniqueNameResult.Failure.DuplicateName -> { + helperText = null + error = binding.root.resources.getString(R.string.toast_duplicate_field) + } + + FieldUtil.UniqueNameResult.Failure.EmptyName -> { + // Differs from the add operation + helperText = binding.root.resources.getString(R.string.toast_empty_name) + error = null + isErrorEnabled = false + } + } + } + } + fun save() { val position = bindingAdapterPosition if (position == RecyclerView.NO_POSITION) return diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index d2f0d4e27ad1..d0fc52ea6a71 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -1,6 +1,5 @@ package com.ichi2.anki.notetype.fieldeditor -import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -22,20 +21,17 @@ import com.ichi2.anki.observability.undoableOp import com.ichi2.anki.servicelayer.LanguageHint import com.ichi2.anki.servicelayer.LanguageHintService import com.ichi2.anki.servicelayer.LanguageHintService.languageHint +import com.ichi2.utils.FieldUtil import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.ankiweb.rsdroid.BackendException -import org.jetbrains.annotations.Contract import timber.log.Timber import java.util.UUID -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract class NoteTypeFieldEditorViewModel( savedStateHandle: SavedStateHandle, @@ -49,21 +45,7 @@ class NoteTypeFieldEditorViewModel( private val hasUndoableOperation get() = fieldsEditOperationStack.value.any { !it.isUndoable } private val _state = MutableStateFlow(NoteTypeFieldEditorState(fields = emptyList())) - /** - * The pair of the field uuid and the new name which is pending confirmation - */ - private val pendingRename = MutableStateFlow("" to "") - val state: Flow = - _state.combine(pendingRename) { state, (uuid, name) -> - Timber.d("current state: $state") - val mutableFields = state.fields.toMutableList() - val pos = mutableFields.indexOfFirst { it.uuid == uuid } - if (pos != NO_POSITION) { - Timber.d("pending rename: override ${mutableFields[pos].name} to $name") - mutableFields[pos] = mutableFields[pos].copy(name = name) - } - return@combine state.copy(fields = mutableFields.toList()) - } + val state: StateFlow = _state.asStateFlow() init { viewModelScope.launch { @@ -82,27 +64,33 @@ class NoteTypeFieldEditorViewModel( ) { Timber.d("addOperationStackAddField") Timber.d("add $name at $position") - uniqueName(name = name).fold( - onSuccess = { validName -> + val nameList = _state.value.fields.map { it.name.savedName } + when (val result = FieldUtil.uniqueName(nameList = nameList, newName = name)) { + is FieldUtil.UniqueNameResult.Success -> { val position = if (position == NO_POSITION) _state.value.fields.lastIndex + 1 else position _state.update { oldValue -> val fields = oldValue.fields.toMutableList() - fields.temporaryAdd(position, validName) + val name = + NoteTypeFieldRowData.Name( + savedName = result.name, + editingName = null, + valid = result, + ) + fields.temporaryAdd(position, name) val action = NoteTypeFieldEditorState.Action.None return@update oldValue.copy(fields = fields.toList(), action = action) } - val operation = NoteTypeFieldOperation.Add(position, validName) + val operation = NoteTypeFieldOperation.Add(position, result.name) fieldsEditOperationStack.update { val mutableStack = it.toMutableList() mutableStack.add(operation) return@update mutableStack.toList() } - }, - onFailure = { resId -> - val action = NoteTypeFieldEditorState.Action.Rejected(resId) - _state.value = _state.value.copy(action = action) - }, - ) + } + + FieldUtil.UniqueNameResult.Failure.DuplicateName -> TODO() + FieldUtil.UniqueNameResult.Failure.EmptyName -> TODO() + } } /** @@ -122,7 +110,7 @@ class NoteTypeFieldEditorViewModel( Timber.d("isEditing: $isEditing") val mutableStack = fieldsEditOperationStack.value.toMutableList() while (true) { - // remove previous rename operation + // remove previous rename operations val last = mutableStack.lastOrNull() if (last is NoteTypeFieldOperation.Rename && last.position == position) { mutableStack.removeAt(mutableStack.lastIndex) @@ -134,47 +122,50 @@ class NoteTypeFieldEditorViewModel( val oldValue = _state.value val oldField = oldValue.fields[position] val oldName = oldField.name - val result = uniqueName(position, name) + val nameList = _state.value.fields.map { it.name.savedName } + val result = FieldUtil.uniqueName(nameList, position, name) Timber.d("rename $oldName to $name at $position") - pendingRename.value = oldField.uuid to name - if (isEditing) { - if (result is UniqueNameResult.Success && oldName == result.name) { - val operation = NoteTypeFieldOperation.Rename(position, oldName, result.name) + + when (result) { + is FieldUtil.UniqueNameResult.Success -> { + val fields = oldValue.fields.toMutableList() + val newName = + if (isEditing) { + oldName.copy(editingName = name, valid = result) + } else { + oldName.copy(savedName = result.name, editingName = null, valid = result) + } + fields.temporaryRename(position, newName) + val action = + NoteTypeFieldEditorState.Action.None + _state.value = oldValue.copy(fields = fields.toList(), action = action) + + if (oldName.savedName == result.name) return + + val operation = + NoteTypeFieldOperation.Rename(position, oldName.savedName, result.name) mutableStack.add(operation) + fieldsEditOperationStack.value = mutableStack.toList() } - fieldsEditOperationStack.value = mutableStack.toList() - val action = NoteTypeFieldEditorState.Action.None - _state.value = oldValue.copy(action = action) - } else { - pendingRename.value = "" to "" - when (result) { - is UniqueNameResult.Success -> { - if (oldName == result.name) { - Timber.d("rename cancelled at $position") - return + is FieldUtil.UniqueNameResult.Failure -> { + Timber.d("rename failed due to $result at $position") + val fields = oldValue.fields.toMutableList() + val newName = + if (isEditing) { + oldName.copy(editingName = name, valid = result) + } else { + oldName.copy(editingName = null, valid = result) } - val operation = NoteTypeFieldOperation.Rename(position, oldName, result.name) - mutableStack.add(operation) - fieldsEditOperationStack.value = mutableStack.toList() - - val fields = oldValue.fields.toMutableList() - fields.temporaryRename(position, result.name) - val action = + fields.temporaryRename(position, newName) + val action = + if (isEditing) { NoteTypeFieldEditorState.Action.None - _state.value = oldValue.copy(fields = fields.toList(), action = action) - } - is UniqueNameResult.Failure -> { - Timber.d("rename failed due to $result at $position") - val action = - if (oldName == name) { - NoteTypeFieldEditorState.Action.None - } else { - NoteTypeFieldEditorState.Action.Rejected(result.resId) - } - _state.value = oldValue.copy(action = action) - } + } else { + NoteTypeFieldEditorState.Action.Rejected(result.resId) + } + _state.value = oldValue.copy(fields = fields.toList(), action = action) } } } @@ -207,7 +198,7 @@ class NoteTypeFieldEditorViewModel( val action = NoteTypeFieldEditorState.Action.Undoable( R.string.model_field_editor_delete_success_result, - arrayListOf(fieldData.name), + arrayListOf(fieldData.name.savedName), ) return@update oldValue.copy(fields = fields.toList(), action = action) } @@ -327,7 +318,7 @@ class NoteTypeFieldEditorViewModel( private fun MutableList.temporaryAdd( position: Int = NO_POSITION, - name: String, + name: NoteTypeFieldRowData.Name, ) { val newField = NoteTypeFieldRowData(name = name) if (position != NO_POSITION) { @@ -339,7 +330,7 @@ class NoteTypeFieldEditorViewModel( private fun MutableList.temporaryRename( position: Int, - newName: String, + newName: NoteTypeFieldRowData.Name, ) { val field = this[position] this[position] = field.copy(name = newName) @@ -382,39 +373,6 @@ class NoteTypeFieldEditorViewModel( this[position] = field.copy(uuid = uuid) } - /** - * Cleans the input field or explain why it's rejected - * @param position the position of the field - * @param name the input - * @return the result UniqueNameResult.Success which contains the unique name or UniqueNameResult.Failure which contains string resource id of the reason why it's rejected - * - */ - private fun uniqueName( - position: Int = NO_POSITION, - name: String, - ): UniqueNameResult { - var input = - name - .replace("[\\n\\r{}:\"]".toRegex(), "") - // The number of #, ^, /, space, tab, starting the input - var offset = 0 - while (offset < input.length) { - if (!listOf('#', '^', '/', ' ', '\t').contains(input[offset])) { - break - } - offset++ - } - input = input.substring(offset).trim() - if (input.isEmpty()) { - return UniqueNameResult.Failure.EmptyName - } - val otherFields = _state.value.fields.filterIndexed { index, _ -> index != position } - if (otherFields.any { it.name == input }) { - return UniqueNameResult.Failure.DuplicateName - } - return UniqueNameResult.Success(input) - } - /** * Undo the last unsaved change */ @@ -434,8 +392,15 @@ class NoteTypeFieldEditorViewModel( when (undoOperation) { is NoteTypeFieldOperation.Add -> temporaryDelete(undoOperation.position) - is NoteTypeFieldOperation.Rename -> - temporaryRename(undoOperation.position, undoOperation.oldName) + is NoteTypeFieldOperation.Rename -> { + val name = + NoteTypeFieldRowData.Name( + savedName = undoOperation.oldName, + editingName = null, + valid = FieldUtil.UniqueNameResult.Success(undoOperation.oldName), + ) + temporaryRename(undoOperation.position, name) + } is NoteTypeFieldOperation.Reposition -> temporaryReposition(undoOperation.newPosition, undoOperation.oldPosition) is NoteTypeFieldOperation.ChangeSort -> @@ -622,8 +587,14 @@ class NoteTypeFieldEditorViewModel( return getNotetype(ntid).run { val sortF = config.sortFieldIdx fieldsList.mapIndexed { index, it -> + val name = + NoteTypeFieldRowData.Name( + savedName = it.name, + editingName = null, + valid = FieldUtil.UniqueNameResult.Success(it.name), + ) NoteTypeFieldRowData( - name = it.name, + name = name, isOrder = index == sortF, locale = languageMap.getOrDefault(it.name, null), ) @@ -750,40 +721,4 @@ class NoteTypeFieldEditorViewModel( private companion object { private const val NO_POSITION = -1 } - - private sealed class UniqueNameResult { - data class Success( - /** - * The unique name of the field - */ - val name: String, - ) : UniqueNameResult() - - sealed class Failure( - /** - * The string resource id of the reason why the name is rejected - */ - @StringRes val resId: Int, - ) : UniqueNameResult() { - object EmptyName : Failure(R.string.toast_empty_name) - - object DuplicateName : Failure(R.string.toast_duplicate_field) - } - - @OptIn(ExperimentalContracts::class) - @Contract - fun fold( - onSuccess: (String) -> Unit, - onFailure: (resId: Int) -> Unit, - ) { - contract { - callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE) - callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE) - } - when (this) { - is Success -> onSuccess(name) - is Failure -> onFailure(resId) - } - } - } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt index e13c5a7cc9d4..0ac5a97e4fd1 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldRowData.kt @@ -1,11 +1,20 @@ package com.ichi2.anki.notetype.fieldeditor +import com.ichi2.utils.FieldUtil import java.util.Locale import java.util.UUID data class NoteTypeFieldRowData( val uuid: String = UUID.randomUUID().toString(), - val name: String, + val name: Name, val isOrder: Boolean = false, val locale: Locale? = null, -) +) { + val displayName get() = name.editingName ?: name.savedName + + data class Name( + val savedName: String, + val editingName: String? = null, + val valid: FieldUtil.UniqueNameResult, + ) +} diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/FieldUtil.kt b/AnkiDroid/src/main/java/com/ichi2/utils/FieldUtil.kt new file mode 100644 index 000000000000..d4ece9f7d4ea --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/utils/FieldUtil.kt @@ -0,0 +1,84 @@ +package com.ichi2.utils + +import androidx.annotation.StringRes +import com.ichi2.anki.R +import com.ichi2.utils.FieldUtil.NO_POSITION +import org.jetbrains.annotations.Contract +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +object FieldUtil { + /** + * Cleans the input field or explain why it's rejected + * @param nameList the list of existing field names + * @param position the position of the field or [NO_POSITION] to add to the end + * @param newName the input + * @return the result UniqueNameResult.Success which contains the unique name or UniqueNameResult.Failure which contains string resource id of the reason why it's rejected + * + */ + fun uniqueName( + nameList: List, + position: Int = NO_POSITION, + newName: String, + ): UniqueNameResult { + var input = + newName + .replace("[\\n\\r{}:\"]".toRegex(), "") + // The number of #, ^, /, space, tab, starting the input + var offset = 0 + while (offset < input.length) { + if (!listOf('#', '^', '/', ' ', '\t').contains(input[offset])) { + break + } + offset++ + } + input = input.substring(offset).trim() + if (input.isEmpty()) { + return UniqueNameResult.Failure.EmptyName + } + val otherFields = nameList.filterIndexed { index, _ -> index != position } + if (otherFields.any { it == input }) { + return UniqueNameResult.Failure.DuplicateName + } + return UniqueNameResult.Success(input) + } + + sealed class UniqueNameResult { + data class Success( + /** + * The unique name of the field + */ + val name: String, + ) : UniqueNameResult() + + sealed class Failure( + /** + * The string resource id of the reason why the name is rejected + */ + @StringRes val resId: Int, + ) : UniqueNameResult() { + object EmptyName : Failure(R.string.toast_empty_name) + + object DuplicateName : Failure(R.string.toast_duplicate_field) + } + + @OptIn(ExperimentalContracts::class) + @Contract + fun fold( + onSuccess: (String) -> Unit, + onFailure: (resId: Int) -> Unit, + ) { + contract { + callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE) + callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE) + } + when (this) { + is Success -> onSuccess(name) + is Failure -> onFailure(resId) + } + } + } + + const val NO_POSITION = -1 +} diff --git a/AnkiDroid/src/main/res/layout/item_notetype_field.xml b/AnkiDroid/src/main/res/layout/item_notetype_field.xml index f455f68e673c..83bfb8e0b3fe 100644 --- a/AnkiDroid/src/main/res/layout/item_notetype_field.xml +++ b/AnkiDroid/src/main/res/layout/item_notetype_field.xml @@ -1,38 +1,31 @@ - + android:minHeight="?android:attr/listPreferredItemHeight"> + + - - - - + app:layout_constraintBottom_toBottomOf="@id/edit_box_center_guideline" + app:layout_constraintEnd_toStartOf="@+id/field_language_button" + app:layout_constraintTop_toTopOf="@id/edit_box_center_guideline" /> - + app:layout_constraintBottom_toBottomOf="@id/edit_box_center_guideline" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/edit_box_center_guideline" /> + + + + + + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/values/17-model-manager.xml b/AnkiDroid/src/main/res/values/17-model-manager.xml index 12d90d499950..4aa1517474df 100644 --- a/AnkiDroid/src/main/res/values/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values/17-model-manager.xml @@ -39,8 +39,10 @@ Add field + Field name Delete field Rename field + Name contained invalid characters; saved as ‘%1$s’ Set keyboard language hint Reposition field Updating fields From 9f276b882f8b818927761567237cb8788440f671 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:17:24 +0900 Subject: [PATCH 16/19] feat: show undo snackbar when renaming a field Re-enabled undo snackbar appearing when renaming a field, to notify users when users scroll recyclerview and the new name is automatically saved. --- .../notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt | 9 ++++++++- AnkiDroid/src/main/res/values/17-model-manager.xml | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index d0fc52ea6a71..e1b63e9b6204 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -138,7 +138,14 @@ class NoteTypeFieldEditorViewModel( } fields.temporaryRename(position, newName) val action = - NoteTypeFieldEditorState.Action.None + if (isEditing) { + NoteTypeFieldEditorState.Action.None + } else { + NoteTypeFieldEditorState.Action.Undoable( + resId = R.string.model_field_editor_rename_success_result, + formatArgs = arrayListOf(oldName.savedName, result.name), + ) + } _state.value = oldValue.copy(fields = fields.toList(), action = action) if (oldName.savedName == result.name) return diff --git a/AnkiDroid/src/main/res/values/17-model-manager.xml b/AnkiDroid/src/main/res/values/17-model-manager.xml index 4aa1517474df..bea16c629b9f 100644 --- a/AnkiDroid/src/main/res/values/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values/17-model-manager.xml @@ -48,6 +48,7 @@ Updating fields Sort by this field Deleted ‘%1$s’ + Renamed ‘%1$s’ to ‘%2$s’ Saved changes successfully Discarded changes These changes cannot be undone. Are you sure you wish to save these changes? From 8713b333f62254f97df3f811281cc02f03a88218 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:28:58 +0900 Subject: [PATCH 17/19] fix: don't show undo snackbar when rename the field to the same name. --- .../notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index e1b63e9b6204..bc949b76245e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -137,8 +137,9 @@ class NoteTypeFieldEditorViewModel( oldName.copy(savedName = result.name, editingName = null, valid = result) } fields.temporaryRename(position, newName) + val hasNoChange = oldName.savedName == result.name val action = - if (isEditing) { + if (isEditing || hasNoChange) { NoteTypeFieldEditorState.Action.None } else { NoteTypeFieldEditorState.Action.Undoable( @@ -148,7 +149,7 @@ class NoteTypeFieldEditorViewModel( } _state.value = oldValue.copy(fields = fields.toList(), action = action) - if (oldName.savedName == result.name) return + if (hasNoChange) return val operation = NoteTypeFieldOperation.Rename(position, oldName.savedName, result.name) From 565b3ad818a928ff5181ca10f975abcab34c5e97 Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:34:21 +0900 Subject: [PATCH 18/19] fix: add code when failed to add the field named the given name. --- .../notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt index bc949b76245e..d94c0066221f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/notetype/fieldeditor/NoteTypeFieldEditorViewModel.kt @@ -87,9 +87,11 @@ class NoteTypeFieldEditorViewModel( return@update mutableStack.toList() } } - - FieldUtil.UniqueNameResult.Failure.DuplicateName -> TODO() - FieldUtil.UniqueNameResult.Failure.EmptyName -> TODO() + is FieldUtil.UniqueNameResult.Failure -> { + Timber.d("add failed due to $result") + val action = NoteTypeFieldEditorState.Action.Rejected(result.resId) + _state.value = _state.value.copy(action = action) + } } } From e55419d3ca533f19222ac6cd35762f8e953f9eee Mon Sep 17 00:00:00 2001 From: S-H-Y-A <74596628+S-H-Y-A@users.noreply.github.com> Date: Sun, 15 Mar 2026 16:26:15 +0900 Subject: [PATCH 19/19] fix: change misleading string --- AnkiDroid/src/main/res/values/17-model-manager.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnkiDroid/src/main/res/values/17-model-manager.xml b/AnkiDroid/src/main/res/values/17-model-manager.xml index bea16c629b9f..547cac24ec8b 100644 --- a/AnkiDroid/src/main/res/values/17-model-manager.xml +++ b/AnkiDroid/src/main/res/values/17-model-manager.xml @@ -42,7 +42,7 @@ Field name Delete field Rename field - Name contained invalid characters; saved as ‘%1$s’ + Name contains invalid characters; will be saved as ‘%1$s’ Set keyboard language hint Reposition field Updating fields