Skip to content

Commit d50e845

Browse files
lukstbitmikehardy
authored andcommitted
Use ViewModel in ManageNoteTypes
All interactions(with the exception of adding) in ManageNoteTypes is now handled by the ViewModel and the UI just updates itself based on the provided states objects.
1 parent d8de96b commit d50e845

File tree

6 files changed

+120
-187
lines changed

6 files changed

+120
-187
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/notetype/AddNewNotesType.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,15 @@ class AddNewNotesType(
150150
selectedOption: AddNotetypeUiModel,
151151
) {
152152
activity.launchCatchingTask {
153-
activity.runAndRefreshAfter {
153+
withCol {
154154
val kind = StockNotetype.Kind.forNumber(selectedOption.id.toInt())
155155
val updatedStandardNotetype =
156156
getStockNotetype(kind).apply {
157157
name = newName
158158
}
159159
addNotetypeLegacy(BackendUtils.toJsonBytes(updatedStandardNotetype))
160160
}
161+
activity.viewModel.refreshNoteTypes()
161162
}
162163
}
163164

@@ -166,7 +167,7 @@ class AddNewNotesType(
166167
model: AddNotetypeUiModel,
167168
) {
168169
activity.launchCatchingTask {
169-
activity.runAndRefreshAfter {
170+
withCol {
170171
val targetNotetype = getNotetype(model.id)
171172
val newNotetype =
172173
targetNotetype.copy {
@@ -175,6 +176,7 @@ class AddNewNotesType(
175176
}
176177
addNotetype(newNotetype)
177178
}
179+
activity.viewModel.refreshNoteTypes()
178180
}
179181
}
180182

AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNoteTypesViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class ManageNoteTypesViewModel : ViewModel() {
125125
_state.update { oldState ->
126126
oldState.copy(isLoading = false, message = UserMessage.DeletingLastModel)
127127
}
128-
OpChanges.getDefaultInstance()
128+
return@undoableOp OpChanges.getDefaultInstance()
129129
}
130130
safeRemoveNoteType(nid)
131131
.onSuccess { changes ->

AnkiDroid/src/main/java/com/ichi2/anki/notetype/ManageNotetypes.kt

Lines changed: 88 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -15,36 +15,32 @@
1515
****************************************************************************************/
1616
package com.ichi2.anki.notetype
1717

18-
import android.annotation.SuppressLint
1918
import android.app.SearchManager
2019
import android.content.Context
2120
import android.content.Intent
2221
import android.os.Bundle
2322
import android.view.Menu
2423
import androidx.activity.result.ActivityResult
2524
import androidx.activity.result.contract.ActivityResultContracts
25+
import androidx.activity.viewModels
2626
import androidx.annotation.StringRes
27-
import androidx.appcompat.app.ActionBar
2827
import androidx.appcompat.app.AlertDialog
2928
import 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
3133
import 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
3535
import com.ichi2.anki.R
36-
import com.ichi2.anki.common.annotations.NeedsTest
3736
import com.ichi2.anki.databinding.ActivityManageNoteTypesBinding
37+
import com.ichi2.anki.dialogs.dismissLoadingDialog
38+
import com.ichi2.anki.dialogs.showLoadingDialog
3839
import 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
4441
import com.ichi2.anki.snackbar.showSnackbar
4542
import com.ichi2.anki.userAcceptsSchemaChange
4643
import com.ichi2.anki.utils.Destination
47-
import com.ichi2.anki.withProgress
4844
import com.ichi2.ui.AccessibleSearchView
4945
import com.ichi2.utils.getInputField
5046
import com.ichi2.utils.input
@@ -54,38 +50,25 @@ import com.ichi2.utils.positiveButton
5450
import com.ichi2.utils.show
5551
import com.ichi2.utils.title
5652
import dev.androidbroadcast.vbpd.viewBinding
57-
import net.ankiweb.rsdroid.BackendException
53+
import kotlinx.coroutines.launch
5854

5955
class 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

273220
class ManageNoteTypesDestination : Destination {

0 commit comments

Comments
 (0)