1515 ****************************************************************************************/
1616package com.ichi2.anki.notetype
1717
18- import android.annotation.SuppressLint
1918import android.app.SearchManager
2019import android.content.Context
2120import android.content.Intent
2221import android.os.Bundle
2322import android.view.Menu
2423import androidx.activity.result.ActivityResult
2524import androidx.activity.result.contract.ActivityResultContracts
25+ import androidx.activity.viewModels
2626import androidx.annotation.StringRes
27- import androidx.appcompat.app.ActionBar
2827import androidx.appcompat.app.AlertDialog
2928import androidx.appcompat.widget.SearchView
30- import anki.notetypes.copy
29+ import androidx.appcompat.widget.Toolbar
30+ import androidx.lifecycle.Lifecycle
31+ import androidx.lifecycle.lifecycleScope
32+ import androidx.lifecycle.repeatOnLifecycle
3133import com.ichi2.anki.AnkiActivity
32- import com.ichi2.anki.CardTemplateEditor
33- import com.ichi2.anki.CollectionManager.withCol
34- import com.ichi2.anki.NoteTypeFieldEditor
34+ import com.ichi2.anki.CrashReportService
3535import com.ichi2.anki.R
36- import com.ichi2.anki.common.annotations.NeedsTest
3736import com.ichi2.anki.databinding.ActivityManageNoteTypesBinding
37+ import com.ichi2.anki.dialogs.dismissLoadingDialog
38+ import com.ichi2.anki.dialogs.showLoadingDialog
3839import com.ichi2.anki.launchCatchingTask
39- import com.ichi2.anki.libanki.getNotetype
40- import com.ichi2.anki.libanki.getNotetypeNameIdUseCount
41- import com.ichi2.anki.libanki.getNotetypeNames
42- import com.ichi2.anki.libanki.removeNotetype
43- import com.ichi2.anki.libanki.updateNotetype
40+ import com.ichi2.anki.notetype.ManageNoteTypesState.UserMessage
4441import com.ichi2.anki.snackbar.showSnackbar
4542import com.ichi2.anki.userAcceptsSchemaChange
4643import com.ichi2.anki.utils.Destination
47- import com.ichi2.anki.withProgress
4844import com.ichi2.ui.AccessibleSearchView
4945import com.ichi2.utils.getInputField
5046import com.ichi2.utils.input
@@ -54,38 +50,25 @@ import com.ichi2.utils.positiveButton
5450import com.ichi2.utils.show
5551import com.ichi2.utils.title
5652import dev.androidbroadcast.vbpd.viewBinding
57- import net.ankiweb.rsdroid.BackendException
53+ import kotlinx.coroutines.launch
5854
5955class ManageNotetypes : AnkiActivity (R .layout.activity_manage_note_types) {
6056 private val binding by viewBinding(ActivityManageNoteTypesBinding ::bind)
61-
62- private lateinit var actionBar: ActionBar
63-
64- private var currentNotetypes: List <ManageNoteTypeUiModel > = emptyList()
65-
66- // Store search query
67- private var searchQuery: String = " "
57+ val viewModel by viewModels<ManageNoteTypesViewModel >()
6858
6959 private val notetypesAdapter: NotetypesAdapter by lazy {
7060 NotetypesAdapter (
7161 this @ManageNotetypes,
72- onShowFields = {
73- launchForChanges<NoteTypeFieldEditor >(
74- mapOf (
75- " title" to it.name,
76- " noteTypeID" to it.id,
77- ),
78- )
79- },
80- onEditCards = { launchForChanges<CardTemplateEditor >(mapOf (" noteTypeId" to it.id)) },
62+ onItemClick = viewModel::onItemClick,
63+ onEditCards = viewModel::onCardEditorRequested,
8164 onRename = ::renameNotetype,
8265 onDelete = ::deleteNotetype,
8366 )
8467 }
8568 private val outsideChangesLauncher =
8669 registerForActivityResult(ActivityResultContracts .StartActivityForResult ()) { result: ActivityResult ->
8770 if (result.resultCode == RESULT_OK ) {
88- launchCatchingTask { runAndRefreshAfter() }
71+ viewModel.refreshNoteTypes()
8972 }
9073 }
9174
@@ -95,21 +78,77 @@ class ManageNotetypes : AnkiActivity(R.layout.activity_manage_note_types) {
9578 }
9679
9780 super .onCreate(savedInstanceState)
98- setTitle(R .string.model_browser_label)
99- actionBar = enableToolbar()
81+ enableToolbar().title = getString(R .string.model_browser_label)
10082 binding.noteTypesList.adapter = notetypesAdapter
10183 binding.floatingActionButton.setOnClickListener {
10284 val addNewNotesType = AddNewNotesType (this )
10385 launchCatchingTask { addNewNotesType.showAddNewNotetypeDialog() }
10486 }
105- launchCatchingTask { runAndRefreshAfter() } // shows the initial note types list
87+ lifecycleScope.launch {
88+ repeatOnLifecycle(Lifecycle .State .STARTED ) {
89+ viewModel.state.collect { state ->
90+ // as they are transient user messages take precedence and are immediately consumed
91+ if (state.message != null ) {
92+ val snackbarMessage =
93+ when (state.message) {
94+ UserMessage .DeletingLastModel -> getString(R .string.toast_last_model)
95+ }
96+ showSnackbar(snackbarMessage)
97+ viewModel.clearMessage()
98+ return @collect
99+ }
100+ // after messages destinations are immediate targets for execution
101+ val currentDestination = state.destination
102+ if (currentDestination != null ) {
103+ viewModel.clearDestination()
104+ outsideChangesLauncher.launch(currentDestination.toIntent(this @ManageNotetypes))
105+ return @collect
106+ }
107+ bindState(state)
108+ }
109+ }
110+ }
111+ }
112+
113+ private fun bindState (state : ManageNoteTypesState ) {
114+ if (state.error != null ) {
115+ if (state.error.isReportable) {
116+ CrashReportService .sendExceptionReport(
117+ state.error.source,
118+ ManageNotetypes ::class .java.simpleName,
119+ )
120+ }
121+ AlertDialog .Builder (this ).show {
122+ message(text = state.error.source.message)
123+ positiveButton(R .string.close) { viewModel.refreshNoteTypes() }
124+ }
125+ viewModel.clearError()
126+ return
127+ }
128+ if (state.isLoading) {
129+ showLoadingDialog()
130+ } else {
131+ dismissLoadingDialog()
132+ }
133+ notetypesAdapter.submitList(state.noteTypes)
134+ supportActionBar?.subtitle =
135+ resources.getQuantityString(
136+ R .plurals.model_browser_types_available,
137+ state.noteTypes.size,
138+ state.noteTypes.size,
139+ )
140+ if (state.searchQuery.isNotEmpty()) {
141+ val searchView =
142+ findViewById<Toolbar >(R .id.toolbar).menu?.findItem(R .id.search_item) as ? AccessibleSearchView
143+ searchView?.setQuery(state.searchQuery, false )
144+ }
106145 }
107146
108147 override fun onCreateOptionsMenu (menu : Menu ): Boolean {
109148 menuInflater.inflate(R .menu.search, menu)
110149
111150 val searchItem = menu.findItem(R .id.search_item)
112- val searchManager = getSystemService(Context . SEARCH_SERVICE ) as SearchManager
151+ val searchManager = getSystemService(SEARCH_SERVICE ) as SearchManager
113152 val searchView = searchItem?.actionView as ? AccessibleSearchView
114153 searchView?.maxWidth = Integer .MAX_VALUE
115154 searchView?.setSearchableInfo(searchManager.getSearchableInfo(componentName))
@@ -119,155 +158,63 @@ class ManageNotetypes : AnkiActivity(R.layout.activity_manage_note_types) {
119158 override fun onQueryTextSubmit (query : String ): Boolean = true
120159
121160 override fun onQueryTextChange (newText : String? ): Boolean {
122- // Update the search query
123- searchQuery = newText.orEmpty( )
124- filterNoteTypes(searchQuery)
161+ if (newText != null && viewModel.state.value.searchQuery != newText) {
162+ viewModel.filter(newText )
163+ }
125164 return true
126165 }
127166 },
128167 )
129168 return true
130169 }
131170
132- /* *
133- * Filters and updates the note types list based on the query
134- */
135- @NeedsTest(" verify note types list still filtered by search query after rename or delete" )
136- private fun filterNoteTypes (query : String ) {
137- val filteredList =
138- if (query.isEmpty()) {
139- currentNotetypes
140- } else {
141- currentNotetypes.filter {
142- it.name.lowercase().contains(query.lowercase())
143- }
144- }
145- notetypesAdapter.submitList(filteredList)
146- }
147-
148- @SuppressLint(" CheckResult" )
149- private fun renameNotetype (manageNoteTypeUiModel : ManageNoteTypeUiModel ) {
171+ private fun renameNotetype (state : NoteTypeItemState ) {
150172 launchCatchingTask {
151- val allNotetypes = mutableListOf<AddNotetypeUiModel >()
152- allNotetypes.addAll(
153- withProgress {
154- withCol { getNotetypeNames().map { it.toUiModel() } }
155- },
156- )
173+ val allNotetypes = viewModel.state.value.noteTypes
157174 val dialog =
158175 AlertDialog
159176 .Builder (this @ManageNotetypes)
160177 .show {
161178 title(R .string.rename_model)
162179 positiveButton(R .string.rename) {
163- launchCatchingTask(
164- // TODO: Change to CardTypeException: https://github.com/ankidroid/Anki-Android-Backend/issues/537
165- // Card template 1 in note type 'character' has a problem.
166- // Expected to find a field replacement on the front of the card template.
167- skipCrashReport = { it is BackendException },
168- ) {
169- runAndRefreshAfter {
170- val initialNotetype = getNotetype(manageNoteTypeUiModel.id)
171- val renamedNotetype =
172- initialNotetype.copy {
173- this .name = (it as AlertDialog ).getInputField().text.toString()
174- }
175- updateNotetype(renamedNotetype)
176- }
177- }
180+ val userInput = (it as AlertDialog ).getInputField().text.toString()
181+ viewModel.rename(state.id, userInput)
178182 }
179183 negativeButton(R .string.dialog_cancel)
180184 setView(R .layout.dialog_generic_text_input)
181185 }.input(
182- prefill = manageNoteTypeUiModel .name,
186+ prefill = state .name,
183187 waitForPositiveButton = false ,
184188 displayKeyboard = true ,
185189 callback = { dialog, text ->
186- dialog.positiveButton.isEnabled =
187- text.isNotEmpty() &&
188- ! allNotetypes
189- .map { it.name }
190- .contains(text.toString())
190+ val isNotADuplicate =
191+ ! allNotetypes.map { it.name }.contains(text.toString())
192+ dialog.positiveButton.isEnabled = text.isNotEmpty() && isNotADuplicate
191193 },
192194 )
193195 // start with the button disabled as dialog shows the initial name
194196 dialog.positiveButton.isEnabled = false
195197 }
196198 }
197199
198- private fun deleteNotetype (manageNoteTypeUiModel : ManageNoteTypeUiModel ) {
200+ private fun deleteNotetype (state : NoteTypeItemState ) {
199201 launchCatchingTask {
200202 @StringRes val messageResourceId: Int? =
201203 if (userAcceptsSchemaChange()) {
202- withProgress {
203- withCol {
204- if (getNotetypeNames().size <= 1 ) {
205- return @withCol null
206- }
207- R .string.model_delete_warning
208- }
209- }
204+ R .string.model_delete_warning
210205 } else {
211206 return @launchCatchingTask
212207 }
213- if (messageResourceId == null ) {
214- showSnackbar(getString(R .string.toast_last_model))
215- return @launchCatchingTask
216- }
217208 AlertDialog .Builder (this @ManageNotetypes).show {
218209 title(R .string.model_browser_delete)
219210 message(messageResourceId)
220211 positiveButton(R .string.dialog_positive_delete) {
221- launchCatchingTask {
222- runAndRefreshAfter { removeNotetype(manageNoteTypeUiModel.id) }
223- }
212+ viewModel.delete(state.id)
224213 }
225214 negativeButton(R .string.dialog_cancel)
226215 }
227216 }
228217 }
229-
230- /* *
231- * Run the provided block on the [Collection](also displaying progress) and then refresh the list
232- * of note types to show the changes. This method expects to be called from the main thread.
233- *
234- * @param action the action to run before the notetypes refresh, if not provided simply refresh
235- */
236- suspend fun runAndRefreshAfter (action : com.ichi2.anki.libanki.Collection .() -> Unit = {}) {
237- val updatedNotetypes =
238- withProgress {
239- withCol {
240- action()
241- getNotetypeNameIdUseCount().map { it.toUiModel() }
242- }
243- }
244-
245- currentNotetypes = updatedNotetypes
246-
247- filterNoteTypes(searchQuery)
248- actionBar.subtitle =
249- resources.getQuantityString(
250- R .plurals.model_browser_types_available,
251- updatedNotetypes.size,
252- updatedNotetypes.size,
253- )
254- }
255-
256- private inline fun <reified T : AnkiActivity > launchForChanges (extras : Map <String , Any >) {
257- val targetIntent =
258- Intent (this @ManageNotetypes, T ::class .java).apply {
259- extras.forEach { toExtra(it) }
260- }
261- outsideChangesLauncher.launch(targetIntent)
262- }
263-
264- private fun Intent.toExtra (newExtra : Map .Entry <String , Any >) {
265- when (newExtra.value) {
266- is String -> putExtra(newExtra.key, newExtra.value as String )
267- is Long -> putExtra(newExtra.key, newExtra.value as Long )
268- else -> throw IllegalArgumentException (" Unexpected value type: ${newExtra.value} " )
269- }
270- }
271218}
272219
273220class ManageNoteTypesDestination : Destination {
0 commit comments