Skip to content

Commit 2906beb

Browse files
committed
refactor(CardTemplateEditor): add ViewModel + State infrastructure
Introduce CardTemplateEditorViewModel and CardTemplateEditorState as the foundation for migrating CardTemplateEditor to use the Rust backend's atomic updateNotetype() API. Part 1 of migration, infrastructure only, no behavioral changes yet.
1 parent 7b8ed69 commit 2906beb

File tree

3 files changed

+207
-0
lines changed

3 files changed

+207
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2025 Snowiee <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki
18+
19+
import com.ichi2.anki.libanki.CardOrdinal
20+
21+
/** Encapsulates the entire state for [CardTemplateEditor] */
22+
data class CardTemplateEditorState(
23+
/** Indicator if the UI should show a "loading" view to the user */
24+
val isLoading: Boolean = true,
25+
/** The currently selected template ordinal (0-based index) */
26+
val currentTemplateOrd: CardOrdinal = 0,
27+
/** The currently selected editor view (front/back/styling) */
28+
val currentEditorViewId: Int = 0,
29+
/** Error that occurred or null for no error */
30+
val error: ReportableException? = null,
31+
/** Simple transient messages in response to user actions or null for no message */
32+
val message: UserMessage? = null,
33+
/** Signal to close the activity after save or discard */
34+
val shouldFinish: Boolean = false,
35+
) {
36+
/** Simple message to be shown to the user */
37+
enum class UserMessage {
38+
CantDeleteLastTemplate,
39+
CantAddTemplateToDynamic,
40+
SaveSuccess,
41+
DeletionWouldOrphanNote,
42+
}
43+
44+
/**
45+
* Wrapper around an exception produced in [CardTemplateEditor] with an extra flag about the
46+
* exception being reportable or not.
47+
*/
48+
data class ReportableException(
49+
val source: Throwable,
50+
/** true if this exception should be sent to [com.ichi2.anki.CrashReportService] */
51+
val isReportable: Boolean = true,
52+
)
53+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2025 Snowiee <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki
18+
19+
import androidx.lifecycle.ViewModel
20+
import com.ichi2.anki.libanki.CardOrdinal
21+
import kotlinx.coroutines.flow.MutableStateFlow
22+
import kotlinx.coroutines.flow.StateFlow
23+
import kotlinx.coroutines.flow.asStateFlow
24+
import kotlinx.coroutines.flow.update
25+
26+
class CardTemplateEditorViewModel : ViewModel() {
27+
private val _state = MutableStateFlow(CardTemplateEditorState())
28+
val state: StateFlow<CardTemplateEditorState> = _state.asStateFlow()
29+
30+
fun setCurrentTemplateOrd(ord: CardOrdinal) {
31+
_state.update { it.copy(currentTemplateOrd = ord) }
32+
}
33+
34+
fun setCurrentEditorView(viewId: Int) {
35+
_state.update { it.copy(currentEditorViewId = viewId) }
36+
}
37+
38+
fun setLoading(isLoading: Boolean) {
39+
_state.update { it.copy(isLoading = isLoading) }
40+
}
41+
42+
fun signalFinish() {
43+
_state.update { it.copy(shouldFinish = true) }
44+
}
45+
46+
fun clearMessage() {
47+
_state.update { it.copy(message = null) }
48+
}
49+
50+
fun clearError() {
51+
_state.update { it.copy(error = null) }
52+
}
53+
54+
fun clearShouldFinish() {
55+
_state.update { it.copy(shouldFinish = false) }
56+
}
57+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (c) 2025 Snowiee <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki
18+
19+
import androidx.test.ext.junit.runners.AndroidJUnit4
20+
import com.ichi2.testutils.JvmTest
21+
import org.junit.Assert.assertEquals
22+
import org.junit.Assert.assertFalse
23+
import org.junit.Test
24+
import org.junit.runner.RunWith
25+
import kotlin.test.assertNull
26+
27+
@RunWith(AndroidJUnit4::class)
28+
class CardTemplateEditorViewModelTest : JvmTest() {
29+
private fun createViewModel() = CardTemplateEditorViewModel()
30+
31+
@Test
32+
fun `initial state is loading with defaults`() {
33+
val viewModel = createViewModel()
34+
val state = viewModel.state.value
35+
36+
assertEquals(true, state.isLoading)
37+
assertEquals(0, state.currentTemplateOrd)
38+
assertEquals(0, state.currentEditorViewId)
39+
assertNull(state.error)
40+
assertNull(state.message)
41+
assertFalse(state.shouldFinish)
42+
}
43+
44+
@Test
45+
fun `setCurrentTemplateOrd updates state correctly`() {
46+
val viewModel = createViewModel()
47+
viewModel.setCurrentTemplateOrd(2)
48+
assertEquals(2, viewModel.state.value.currentTemplateOrd)
49+
}
50+
51+
@Test
52+
fun `setCurrentEditorView updates state correctly`() {
53+
val viewModel = createViewModel()
54+
viewModel.setCurrentEditorView(123)
55+
assertEquals(123, viewModel.state.value.currentEditorViewId)
56+
}
57+
58+
@Test
59+
fun `setLoading updates isLoading state`() {
60+
val viewModel = createViewModel()
61+
viewModel.setLoading(false)
62+
assertFalse(viewModel.state.value.isLoading)
63+
viewModel.setLoading(true)
64+
assertEquals(true, viewModel.state.value.isLoading)
65+
}
66+
67+
@Test
68+
fun `signalFinish sets shouldFinish flag`() {
69+
val viewModel = createViewModel()
70+
assertFalse(viewModel.state.value.shouldFinish)
71+
viewModel.signalFinish()
72+
assertEquals(true, viewModel.state.value.shouldFinish)
73+
}
74+
75+
@Test
76+
fun `clearShouldFinish resets the flag`() {
77+
val viewModel = createViewModel()
78+
viewModel.signalFinish()
79+
assertEquals(true, viewModel.state.value.shouldFinish)
80+
viewModel.clearShouldFinish()
81+
assertFalse(viewModel.state.value.shouldFinish)
82+
}
83+
84+
@Test
85+
fun `clearMessage resets message to null`() {
86+
val viewModel = createViewModel()
87+
viewModel.clearMessage()
88+
assertNull(viewModel.state.value.message)
89+
}
90+
91+
@Test
92+
fun `clearError resets error to null`() {
93+
val viewModel = createViewModel()
94+
viewModel.clearError()
95+
assertNull(viewModel.state.value.error)
96+
}
97+
}

0 commit comments

Comments
 (0)