1616
1717package com.ichi2.anki.notetype
1818
19+ import androidx.annotation.VisibleForTesting
20+ import androidx.annotation.VisibleForTesting.Companion.PRIVATE
1921import androidx.lifecycle.ViewModel
2022import androidx.lifecycle.viewModelScope
2123import anki.collection.OpChanges
2224import anki.notetypes.Notetype
2325import anki.notetypes.copy
2426import com.ichi2.anki.CollectionManager.withCol
27+ import com.ichi2.anki.exception.CombinedException
2528import com.ichi2.anki.libanki.Collection
2629import com.ichi2.anki.libanki.NoteTypeId
2730import com.ichi2.anki.libanki.getNotetype
@@ -40,17 +43,18 @@ import kotlinx.coroutines.flow.asStateFlow
4043import kotlinx.coroutines.flow.update
4144import kotlinx.coroutines.launch
4245import net.ankiweb.rsdroid.BackendException
46+ import timber.log.Timber
4347
4448class ManageNoteTypesViewModel : ViewModel () {
4549 private val _state = MutableStateFlow (ManageNoteTypesState ())
4650 val state: StateFlow <ManageNoteTypesState > = _state .asStateFlow()
47- private lateinit var initialNoteTypes: List <NoteTypeItemState >
4851
4952 init {
5053 refreshNoteTypes()
5154 }
5255
5356 fun refreshNoteTypes () {
57+ Timber .i(" Refreshing list of notetypes" )
5458 _state .update { oldState -> oldState.copy(isLoading = true ) }
5559 viewModelScope.launch {
5660 withCol { safeGetNotetypeNameIdUseCount() }
@@ -59,7 +63,6 @@ class ManageNoteTypesViewModel : ViewModel() {
5963 oldState.copy(isLoading = false , error = ReportableException (it))
6064 }
6165 }.onSuccess {
62- initialNoteTypes = it
6366 _state .update { oldState ->
6467 oldState.copy(isLoading = false , noteTypes = it)
6568 }
@@ -68,11 +71,12 @@ class ManageNoteTypesViewModel : ViewModel() {
6871 }
6972
7073 fun filter (query : String ) {
71- val matchedNoteTypes =
72- initialNoteTypes.filter { entry ->
73- entry.name.contains(query)
74- }
74+ Timber .i(" Filtering list of notetypes with query=$query " )
7575 _state .update { oldState ->
76+ val matchedNoteTypes =
77+ oldState.noteTypes.map {
78+ it.copy(shouldBeDisplayed = it.name.contains(query))
79+ }
7680 oldState.copy(isLoading = false , noteTypes = matchedNoteTypes, searchQuery = query)
7781 }
7882 }
@@ -81,21 +85,15 @@ class ManageNoteTypesViewModel : ViewModel() {
8185 nid : NoteTypeId ,
8286 name : String ,
8387 ) {
88+ Timber .i(" Renaming notetype with id $nid " )
8489 _state .update { oldState -> oldState.copy(isLoading = true ) }
8590 viewModelScope.launch {
8691 undoableOp<OpChanges > {
8792 safeRenameNoteType(nid, name)
8893 .onSuccess { changes ->
8994 _state .update { oldState ->
9095 val updatedNoteTypes =
91- oldState.noteTypes
92- .map { noteTypeState ->
93- if (noteTypeState.id == nid) {
94- noteTypeState.copy(name = name)
95- } else {
96- noteTypeState
97- }
98- }.also { initialNoteTypes = it }
96+ oldState.noteTypes.withUpdatedItem(nid) { old -> old.copy(name = name) }
9997 oldState.copy(isLoading = false , noteTypes = updatedNoteTypes)
10098 }
10199 return @undoableOp changes
@@ -116,7 +114,9 @@ class ManageNoteTypesViewModel : ViewModel() {
116114 }
117115 }
118116
117+ /* * Deletes the note type with [nid] and also updates the multi select mode status if needed */
119118 fun delete (nid : NoteTypeId ) {
119+ Timber .i(" Deleting notetype with id $nid " )
120120 _state .update { oldState -> oldState.copy(isLoading = true ) }
121121 val noteTypesCount = _state .value.noteTypes.size
122122 viewModelScope.launch {
@@ -130,11 +130,12 @@ class ManageNoteTypesViewModel : ViewModel() {
130130 safeRemoveNoteType(nid)
131131 .onSuccess { changes ->
132132 _state .update { oldState ->
133- val updatedNoteTypes =
134- oldState.noteTypes
135- .filter { it.id != nid }
136- .also { initialNoteTypes = it }
137- oldState.copy(isLoading = false , noteTypes = updatedNoteTypes)
133+ val updatedNoteTypes = oldState.noteTypes.filterNot { it.id == nid }
134+ oldState.copy(
135+ isLoading = false ,
136+ noteTypes = updatedNoteTypes,
137+ isInMultiSelectMode = updatedNoteTypes.multiSelectModeStatus,
138+ )
138139 }
139140 return @undoableOp changes
140141 }.onFailure {
@@ -149,30 +150,177 @@ class ManageNoteTypesViewModel : ViewModel() {
149150 }
150151
151152 fun onItemClick (entry : NoteTypeItemState ) {
153+ if (_state .value.isInMultiSelectMode) {
154+ Timber .i(" onItemClick: already in multiple selection mode, toggling selection for notetype with id: ${entry.id} " )
155+ _state .update { oldState ->
156+ val updatedNoteTypes =
157+ oldState.noteTypes.withUpdatedItem(entry.id) { noteType ->
158+ noteType.copy(isSelected = ! noteType.isSelected)
159+ }
160+ oldState.copy(
161+ noteTypes = updatedNoteTypes,
162+ isInMultiSelectMode = updatedNoteTypes.multiSelectModeStatus,
163+ )
164+ }
165+ } else {
166+ Timber .i(" onItemClick: not in multiple selection mode, sending show fields editor request" )
167+ _state .update { oldState ->
168+ oldState.copy(destination = FieldsEditor (entry.id, entry.name))
169+ }
170+ }
171+ }
172+
173+ fun onItemLongClick (entry : NoteTypeItemState ) {
174+ if (_state .value.isInMultiSelectMode) {
175+ Timber .i(" onItemLongClick: already in multiple selection mode, toggling selection for notetype with id: ${entry.id} " )
176+ _state .update { oldState ->
177+ val updatedNoteTypes =
178+ oldState.noteTypes.withUpdatedItem(entry.id) { noteType ->
179+ noteType.copy(isSelected = ! noteType.isSelected)
180+ }
181+ oldState.copy(
182+ noteTypes = updatedNoteTypes,
183+ isInMultiSelectMode = updatedNoteTypes.multiSelectModeStatus,
184+ )
185+ }
186+ } else {
187+ Timber .i(" onItemLongClick: no previous selection, starting multi select mode with notetype(${entry.id} ) selected" )
188+ _state .update { oldState ->
189+ val updatedNoteTypes =
190+ oldState.noteTypes.withUpdatedItem(entry.id) { noteType ->
191+ noteType.copy(isSelected = true )
192+ }
193+ oldState.copy(noteTypes = updatedNoteTypes, isInMultiSelectMode = true )
194+ }
195+ }
196+ }
197+
198+ /* * Updates the check status for a selected note type also updates the multi select mode status if needed */
199+ fun onItemChecked (
200+ entry : NoteTypeItemState ,
201+ isChecked : Boolean ,
202+ ) {
203+ Timber .i(" onItemCheck: update selection for notetype(${entry.id} ) with new status: $isChecked " )
204+ _state .update { oldState ->
205+ val updatedNoteTypes =
206+ _state .value.noteTypes.withUpdatedItem(entry.id) { old -> old.copy(isSelected = isChecked) }
207+ oldState.copy(
208+ noteTypes = updatedNoteTypes,
209+ isInMultiSelectMode = updatedNoteTypes.multiSelectModeStatus,
210+ )
211+ }
212+ }
213+
214+ /* * Clears any selected note types and also exits the multi select mode */
215+ fun clearSelection () {
216+ Timber .i(" Clearing selected notetypes" )
217+ _state .update { oldState ->
218+ val updatedNoteTypes =
219+ oldState.noteTypes.map { noteTypeItemState ->
220+ noteTypeItemState.copy(isSelected = false )
221+ }
222+ oldState.copy(
223+ noteTypes = updatedNoteTypes,
224+ isInMultiSelectMode = updatedNoteTypes.multiSelectModeStatus,
225+ )
226+ }
227+ }
228+
229+ /* *
230+ * Deletes all the [NoteTypeItemState] which are currently selected. Any errors when deleting
231+ * the [Notetype]s will be combined into a single [CombinedException] to present to the user.
232+ */
233+ fun deleteSelectedNoteTypes () {
234+ val noteTypesToDelete = selectedNoteTypes.toMutableList()
235+ Timber .i(" Deleting currently selected notetypes: ${noteTypesToDelete.map { it.id }} " )
236+ // show loading and clear selection
152237 _state .update { oldState ->
153- oldState.copy(destination = FieldsEditor (entry.id, entry.name))
238+ val updateNotetypes =
239+ oldState.noteTypes.map { noteType ->
240+ noteType.copy(isSelected = false )
241+ }
242+ oldState.copy(
243+ isLoading = true ,
244+ noteTypes = updateNotetypes,
245+ isInMultiSelectMode = false ,
246+ )
247+ }
248+ viewModelScope.launch {
249+ val errors = mutableMapOf<NoteTypeItemState , Throwable >()
250+ noteTypesToDelete.forEach { noteType ->
251+ undoableOp<OpChanges > {
252+ safeRemoveNoteType(noteType.id)
253+ .onFailure { exception ->
254+ errors[noteType] = exception
255+ OpChanges .getDefaultInstance()
256+ }.onSuccess { return @undoableOp it }
257+ OpChanges .getDefaultInstance()
258+ }
259+ }
260+ // look through any errors we might have and remove from our list of note types the ones
261+ // that were in noteTypesToDelete but not in errors map(which presumably weren't deleted)
262+ val removedIds = noteTypesToDelete.map { it.id } - errors.keys.map { it.id }.toSet()
263+ val updatedNoteTypes = _state .value.noteTypes.filterNot { it.id in removedIds }
264+ val combinedException =
265+ CombinedException .from(
266+ errors.map { (state, throwable) ->
267+ " ${state.name} - $throwable " to throwable
268+ },
269+ )
270+ _state .update { oldState ->
271+ oldState.copy(
272+ isLoading = false ,
273+ noteTypes = updatedNoteTypes,
274+ error = combinedException?.let { throwable -> ReportableException (throwable) },
275+ )
276+ }
154277 }
155278 }
156279
157280 fun onCardEditorRequested (entry : NoteTypeItemState ) {
281+ Timber .i(" Sending open card editor request" )
158282 _state .update { oldState ->
159283 oldState.copy(destination = CardEditor (entry.id))
160284 }
161285 }
162286
287+ /* * Clears any previous user messages from the state */
163288 fun clearMessage () {
289+ Timber .i(" Clearing user message from state" )
164290 _state .update { oldState -> oldState.copy(message = null ) }
165291 }
166292
293+ /* * Clears any previous errors from the state */
167294 fun clearError () {
295+ Timber .i(" Clearing errors from state" )
168296 _state .update { oldState -> oldState.copy(error = null ) }
169297 }
170298
299+ /* * Clears any previous destinations requested by the user from the state */
171300 fun clearDestination () {
301+ Timber .i(" Clearing requested destinations from state" )
172302 _state .update { oldState -> oldState.copy(destination = null ) }
173303 }
304+
305+ /* *
306+ * Returns a new list where all items are unchanged with the exception of the [NoteTypeItemState]
307+ * identified by [nid] which is replaced by the result of invoking the [update] lambda.
308+ */
309+ private fun List<NoteTypeItemState>.withUpdatedItem (
310+ nid : NoteTypeId ,
311+ update : (NoteTypeItemState ) -> NoteTypeItemState ,
312+ ): List <NoteTypeItemState > = map { noteType -> if (noteType.id == nid) update(noteType) else noteType }
313+
314+ /* * True if we are selecting multiple items(implies at least one item is currently selected), false otherwise. */
315+ @VisibleForTesting(otherwise = PRIVATE )
316+ val List <NoteTypeItemState >.multiSelectModeStatus: Boolean
317+ get() = any { it.isSelected }
174318}
175319
320+ /* * The list of [NoteTypeItemState] that are currently selected */
321+ val ManageNoteTypesViewModel .selectedNoteTypes: List <NoteTypeItemState >
322+ get() = state.value.noteTypes.filter { it.isSelected }
323+
176324private fun Collection.safeRenameNoteType (
177325 nid : NoteTypeId ,
178326 newName : String ,
0 commit comments