feat(ui): édition contact notes profil et rescan carte

EBO <eric.bouhana@softalys.com> committé le 2026-07-22 15:47

03008de33d35ce31304401559bee925a3dacd126

1 parent(s)

9 fichiers modifiés +476 -11
M android/app/src/main/java/fr/ebii/card2vcf/ui/ContactDraftScreen.kt
+26 -5
@@ -43,7 +43,10 @@ fun ContactDraftScreen(
43 43 onExportVcf: () -> Unit,
44 44 onRetry: () -> Unit,
45 45 modifier: Modifier = Modifier,
46 + /** Mode rescan : bouton primary « Mettre à jour ». */
47 + onUpdateContact: (() -> Unit)? = null,
46 48 ) {
49 + val isUpdate = onUpdateContact != null
47 50 Column(
48 51 modifier.fillMaxSize().background(Fond).padding(horizontal = 18.dp, vertical = 16.dp),
49 52 verticalArrangement = Arrangement.spacedBy(14.dp),
@@ -63,22 +66,40 @@ fun ContactDraftScreen(
63 66 Text(draft.rawOcrText, fontFamily = FontFamily.Monospace, color = TexteFaible)
64 67 }
65 68 }
66 - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
69 + if (isUpdate) {
67 70 Button(
68 - onClick = onCreateContact,
69 - modifier = Modifier.weight(1f),
71 + onClick = onUpdateContact,
72 + modifier = Modifier.fillMaxWidth(),
70 73 shape = Square,
71 74 colors = ButtonDefaults.buttonColors(containerColor = Ink, contentColor = OnPrimary),
72 75 ) {
73 - Text(stringResource(R.string.scan_creer_contact))
76 + Text(stringResource(R.string.edit_mettre_a_jour))
74 77 }
75 78 OutlinedButton(
76 79 onClick = onExportVcf,
77 - modifier = Modifier.weight(1f),
80 + modifier = Modifier.fillMaxWidth(),
78 81 shape = Square,
79 82 ) {
80 83 Text(stringResource(R.string.scan_exporter_vcf))
81 84 }
85 + } else {
86 + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
87 + Button(
88 + onClick = onCreateContact,
89 + modifier = Modifier.weight(1f),
90 + shape = Square,
91 + colors = ButtonDefaults.buttonColors(containerColor = Ink, contentColor = OnPrimary),
92 + ) {
93 + Text(stringResource(R.string.scan_creer_contact))
94 + }
95 + OutlinedButton(
96 + onClick = onExportVcf,
97 + modifier = Modifier.weight(1f),
98 + shape = Square,
99 + ) {
100 + Text(stringResource(R.string.scan_exporter_vcf))
101 + }
102 + }
82 103 }
83 104 OutlinedButton(
84 105 onClick = onRetry,
M android/app/src/main/java/fr/ebii/card2vcf/ui/ScanCarteScreen.kt
+8 -0
@@ -56,9 +56,16 @@ fun ScanCarteScreen(
56 56 onBack: () -> Unit,
57 57 onCreateContact: (fr.ebii.card2vcf.contact.ContactCard) -> Unit,
58 58 onExportVcf: (fr.ebii.card2vcf.contact.ContactCard) -> Unit,
59 + onContactUpdated: (contactId: Long) -> Unit,
59 60 modifier: Modifier = Modifier,
60 61 ) {
61 62 val state by viewModel.state.collectAsState()
63 + val saveDoneId by viewModel.saveDoneId.collectAsState()
64 + LaunchedEffect(saveDoneId) {
65 + val id = saveDoneId ?: return@LaunchedEffect
66 + viewModel.consumeSaveDone()
67 + onContactUpdated(id)
68 + }
62 69 when (val s = state) {
63 70 is ScanUiState.DraftReady -> ContactDraftScreen(
64 71 draft = s.draft,
@@ -66,6 +73,7 @@ fun ScanCarteScreen(
66 73 onCreateContact = { onCreateContact(s.draft.card) },
67 74 onExportVcf = { onExportVcf(s.draft.card) },
68 75 onRetry = viewModel::reset,
76 + onUpdateContact = if (viewModel.isEditing) viewModel::updateFromDraft else null,
69 77 modifier = modifier,
70 78 )
71 79 ScanUiState.Scanning -> ScanProgress(R.string.scan_en_cours)
M android/app/src/main/java/fr/ebii/card2vcf/ui/ScanCarteViewModel.kt
+40 -1
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
6 6 import androidx.lifecycle.viewModelScope
7 7 import fr.ebii.card2vcf.contact.ContactCard
8 8 import fr.ebii.card2vcf.contact.ContactDraftMerge
9 +import fr.ebii.card2vcf.data.ContactRepository
9 10 import fr.ebii.card2vcf.ocr.OcrEngine
10 11 import fr.ebii.card2vcf.ocr.OcrException
11 12 import fr.ebii.card2vcf.scan.ScanEngine
@@ -36,11 +37,21 @@ data class ContactDraftUi(
36 37 class ScanCarteViewModel(
37 38 private val scanEngine: ScanEngine,
38 39 private val ocrEngine: OcrEngine,
40 + private val repository: ContactRepository,
39 41 ) : ViewModel() {
40 42
43 + /** Non-null when rescanning an existing contact (update path). */
44 + var editingContactId: Long? = null
45 +
46 + val isEditing: Boolean
47 + get() = editingContactId != null
48 +
41 49 private val _state = MutableStateFlow<ScanUiState>(ScanUiState.Idle)
42 50 val state: StateFlow<ScanUiState> = _state.asStateFlow()
43 51
52 + private val _saveDoneId = MutableStateFlow<Long?>(null)
53 + val saveDoneId: StateFlow<Long?> = _saveDoneId.asStateFlow()
54 +
44 55 fun onCaptureReady() {
45 56 _state.value = ScanUiState.Capturing
46 57 }
@@ -53,9 +64,16 @@ class ScanCarteViewModel(
53 64 _state.value = ScanUiState.OcrRunning
54 65 val ocr = withContext(Dispatchers.Default) { ocrEngine.recognize(scanned.bitmap) }
55 66 _state.value = ScanUiState.Structuring
56 - val merged = withContext(Dispatchers.Default) {
67 + var merged = withContext(Dispatchers.Default) {
57 68 ContactDraftMerge.merge(null, ocr)
58 69 }
70 + val editId = editingContactId
71 + if (editId != null) {
72 + val existing = withContext(Dispatchers.IO) { repository.getById(editId) }
73 + if (existing != null && !existing.notes.isNullOrBlank()) {
74 + merged = merged.copy(note = existing.notes)
75 + }
76 + }
59 77 _state.value = ScanUiState.DraftReady(
60 78 ContactDraftUi(
61 79 card = merged,
@@ -83,6 +101,27 @@ class ScanCarteViewModel(
83 101 }
84 102 }
85 103
104 + /** Met à jour le contact existant (mode rescan) à partir du brouillon courant. */
105 + fun updateFromDraft() {
106 + val editId = editingContactId ?: return
107 + val draft = (_state.value as? ScanUiState.DraftReady)?.draft ?: return
108 + viewModelScope.launch {
109 + withContext(Dispatchers.IO) {
110 + repository.updateFromCard(
111 + id = editId,
112 + card = draft.card,
113 + cardBitmap = draft.preview,
114 + profileBitmap = null,
115 + )
116 + }
117 + _saveDoneId.value = editId
118 + }
119 + }
120 +
121 + fun consumeSaveDone() {
122 + _saveDoneId.value = null
123 + }
124 +
86 125 fun reset() {
87 126 _state.value = ScanUiState.Idle
88 127 }
A android/app/src/main/java/fr/ebii/card2vcf/ui/contact/ContactEditScreen.kt
+271 -0
@@ -0,0 +1,271 @@
1 +package fr.ebii.card2vcf.ui.contact
2 +
3 +import android.graphics.BitmapFactory
4 +import android.net.Uri
5 +import androidx.activity.compose.rememberLauncherForActivityResult
6 +import androidx.activity.result.contract.ActivityResultContracts
7 +import androidx.compose.foundation.Image
8 +import androidx.compose.foundation.background
9 +import androidx.compose.foundation.border
10 +import androidx.compose.foundation.layout.Arrangement
11 +import androidx.compose.foundation.layout.Box
12 +import androidx.compose.foundation.layout.Column
13 +import androidx.compose.foundation.layout.Row
14 +import androidx.compose.foundation.layout.Spacer
15 +import androidx.compose.foundation.layout.fillMaxSize
16 +import androidx.compose.foundation.layout.fillMaxWidth
17 +import androidx.compose.foundation.layout.height
18 +import androidx.compose.foundation.layout.padding
19 +import androidx.compose.foundation.layout.size
20 +import androidx.compose.foundation.rememberScrollState
21 +import androidx.compose.foundation.shape.RoundedCornerShape
22 +import androidx.compose.foundation.verticalScroll
23 +import androidx.compose.material.icons.Icons
24 +import androidx.compose.material.icons.automirrored.outlined.ArrowBack
25 +import androidx.compose.material3.Button
26 +import androidx.compose.material3.ButtonDefaults
27 +import androidx.compose.material3.CircularProgressIndicator
28 +import androidx.compose.material3.Icon
29 +import androidx.compose.material3.IconButton
30 +import androidx.compose.material3.MaterialTheme
31 +import androidx.compose.material3.OutlinedButton
32 +import androidx.compose.material3.Text
33 +import androidx.compose.runtime.Composable
34 +import androidx.compose.runtime.LaunchedEffect
35 +import androidx.compose.runtime.collectAsState
36 +import androidx.compose.runtime.getValue
37 +import androidx.compose.runtime.mutableStateOf
38 +import androidx.compose.runtime.remember
39 +import androidx.compose.runtime.setValue
40 +import androidx.compose.ui.Alignment
41 +import androidx.compose.ui.Modifier
42 +import androidx.compose.ui.graphics.asImageBitmap
43 +import androidx.compose.ui.layout.ContentScale
44 +import androidx.compose.ui.platform.LocalContext
45 +import androidx.compose.ui.res.stringResource
46 +import androidx.compose.ui.unit.dp
47 +import androidx.core.content.FileProvider
48 +import fr.ebii.card2vcf.R
49 +import fr.ebii.card2vcf.contact.ContactCard
50 +import fr.ebii.card2vcf.ui.ContactDraftFields
51 +import fr.ebii.card2vcf.ui.theme.Bordure
52 +import fr.ebii.card2vcf.ui.theme.Encre
53 +import fr.ebii.card2vcf.ui.theme.Fond
54 +import fr.ebii.card2vcf.ui.theme.Ink
55 +import fr.ebii.card2vcf.ui.theme.OnPrimary
56 +import fr.ebii.card2vcf.ui.theme.Surface
57 +import fr.ebii.card2vcf.ui.theme.TexteFaible
58 +import java.io.File
59 +
60 +private val Square = RoundedCornerShape(0.dp)
61 +
62 +@Composable
63 +fun ContactEditScreen(
64 + viewModel: ContactEditViewModel,
65 + onBack: () -> Unit,
66 + onSaved: () -> Unit,
67 + onRescan: (contactId: Long) -> Unit,
68 + modifier: Modifier = Modifier,
69 +) {
70 + val card by viewModel.card.collectAsState()
71 + val profilePreview by viewModel.profilePreview.collectAsState()
72 + val loaded by viewModel.loaded.collectAsState()
73 + val saveDone by viewModel.saveDone.collectAsState()
74 +
75 + LaunchedEffect(saveDone) {
76 + if (saveDone) onSaved()
77 + }
78 +
79 + val context = LocalContext.current
80 + var pendingCaptureUri by remember { mutableStateOf<Uri?>(null) }
81 +
82 + val takePicture = rememberLauncherForActivityResult(
83 + ActivityResultContracts.TakePicture(),
84 + ) { success ->
85 + if (!success) return@rememberLauncherForActivityResult
86 + val uri = pendingCaptureUri ?: return@rememberLauncherForActivityResult
87 + context.contentResolver.openInputStream(uri)?.use { stream ->
88 + BitmapFactory.decodeStream(stream)?.let(viewModel::setProfileBitmap)
89 + }
90 + }
91 +
92 + val pickImage = rememberLauncherForActivityResult(
93 + ActivityResultContracts.GetContent(),
94 + ) { uri ->
95 + uri ?: return@rememberLauncherForActivityResult
96 + context.contentResolver.openInputStream(uri)?.use { stream ->
97 + BitmapFactory.decodeStream(stream)?.let(viewModel::setProfileBitmap)
98 + }
99 + }
100 +
101 + Column(
102 + modifier
103 + .fillMaxSize()
104 + .background(Fond),
105 + ) {
106 + Row(
107 + Modifier
108 + .fillMaxWidth()
109 + .background(Surface)
110 + .padding(horizontal = 4.dp, vertical = 4.dp),
111 + verticalAlignment = Alignment.CenterVertically,
112 + ) {
113 + IconButton(onClick = onBack) {
114 + Icon(
115 + Icons.AutoMirrored.Outlined.ArrowBack,
116 + contentDescription = stringResource(R.string.scan_retour),
117 + )
118 + }
119 + Text(
120 + stringResource(R.string.edit_titre),
121 + style = MaterialTheme.typography.titleMedium,
122 + color = Encre,
123 + modifier = Modifier.padding(start = 4.dp),
124 + )
125 + }
126 +
127 + if (!loaded) {
128 + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
129 + CircularProgressIndicator()
130 + }
131 + return@Column
132 + }
133 +
134 + Column(
135 + Modifier
136 + .weight(1f)
137 + .fillMaxWidth()
138 + .verticalScroll(rememberScrollState())
139 + .padding(horizontal = 18.dp, vertical = 12.dp),
140 + verticalArrangement = Arrangement.spacedBy(14.dp),
141 + ) {
142 + Text(stringResource(R.string.edit_photo_profil), color = Encre)
143 + val preview = profilePreview
144 + if (preview != null) {
145 + Image(
146 + bitmap = preview.asImageBitmap(),
147 + contentDescription = stringResource(R.string.fiche_photo_profil),
148 + modifier = Modifier
149 + .size(96.dp)
150 + .border(1.dp, Bordure, Square),
151 + contentScale = ContentScale.Crop,
152 + )
153 + } else {
154 + Box(
155 + Modifier
156 + .size(96.dp)
157 + .border(1.dp, Bordure, Square)
158 + .background(Surface),
159 + )
160 + }
161 + Row(
162 + Modifier.fillMaxWidth(),
163 + horizontalArrangement = Arrangement.spacedBy(10.dp),
164 + ) {
165 + OutlinedButton(
166 + onClick = {
167 + val dir = File(context.cacheDir, "images").also { it.mkdirs() }
168 + val file = File(dir, "profile_${System.currentTimeMillis()}.jpg")
169 + val uri = FileProvider.getUriForFile(
170 + context,
171 + "${context.packageName}.fileprovider",
172 + file,
173 + )
174 + pendingCaptureUri = uri
175 + takePicture.launch(uri)
176 + },
177 + modifier = Modifier.weight(1f),
178 + shape = Square,
179 + ) {
180 + Text(stringResource(R.string.edit_prendre_photo))
181 + }
182 + OutlinedButton(
183 + onClick = { pickImage.launch("image/*") },
184 + modifier = Modifier.weight(1f),
185 + shape = Square,
186 + ) {
187 + Text(stringResource(R.string.edit_choisir_photo))
188 + }
189 + }
190 +
191 + ContactDraftFields(card = card, onChange = viewModel::updateCard)
192 +
193 + OutlinedButton(
194 + onClick = { onRescan(viewModel.contactId) },
195 + modifier = Modifier.fillMaxWidth(),
196 + shape = Square,
197 + ) {
198 + Text(stringResource(R.string.edit_rescanner))
199 + }
200 + }
201 +
202 + Button(
203 + onClick = viewModel::save,
204 + modifier = Modifier
205 + .fillMaxWidth()
206 + .padding(horizontal = 18.dp, vertical = 12.dp),
207 + shape = Square,
208 + colors = ButtonDefaults.buttonColors(containerColor = Ink, contentColor = OnPrimary),
209 + ) {
210 + Text(stringResource(R.string.edit_enregistrer))
211 + }
212 + }
213 +}
214 +
215 +@Composable
216 +fun ManualContactScreen(
217 + onSave: (ContactCard) -> Unit,
218 + onBack: () -> Unit,
219 + modifier: Modifier = Modifier,
220 +) {
221 + var card by remember { mutableStateOf(ContactCard()) }
222 +
223 + Column(
224 + modifier
225 + .fillMaxSize()
226 + .background(Fond),
227 + ) {
228 + Row(
229 + Modifier
230 + .fillMaxWidth()
231 + .background(Surface)
232 + .padding(horizontal = 4.dp, vertical = 4.dp),
233 + verticalAlignment = Alignment.CenterVertically,
234 + ) {
235 + IconButton(onClick = onBack) {
236 + Icon(
237 + Icons.AutoMirrored.Outlined.ArrowBack,
238 + contentDescription = stringResource(R.string.scan_retour),
239 + )
240 + }
241 + Text(
242 + stringResource(R.string.manual_titre),
243 + style = MaterialTheme.typography.titleMedium,
244 + color = Encre,
245 + modifier = Modifier.padding(start = 4.dp),
246 + )
247 + }
248 + Column(
249 + Modifier
250 + .weight(1f)
251 + .fillMaxWidth()
252 + .verticalScroll(rememberScrollState())
253 + .padding(horizontal = 18.dp, vertical = 12.dp),
254 + verticalArrangement = Arrangement.spacedBy(14.dp),
255 + ) {
256 + Text(stringResource(R.string.manual_sous_titre), color = TexteFaible)
257 + ContactDraftFields(card = card, onChange = { card = it })
258 + Spacer(Modifier.height(8.dp))
259 + }
260 + Button(
261 + onClick = { onSave(card) },
262 + modifier = Modifier
263 + .fillMaxWidth()
264 + .padding(horizontal = 18.dp, vertical = 12.dp),
265 + shape = Square,
266 + colors = ButtonDefaults.buttonColors(containerColor = Ink, contentColor = OnPrimary),
267 + ) {
268 + Text(stringResource(R.string.edit_enregistrer))
269 + }
270 + }
271 +}
A android/app/src/main/java/fr/ebii/card2vcf/ui/contact/ContactEditViewModel.kt
+65 -0
@@ -0,0 +1,65 @@
1 +package fr.ebii.card2vcf.ui.contact
2 +
3 +import android.graphics.Bitmap
4 +import android.graphics.BitmapFactory
5 +import androidx.lifecycle.ViewModel
6 +import androidx.lifecycle.viewModelScope
7 +import fr.ebii.card2vcf.contact.ContactCard
8 +import fr.ebii.card2vcf.data.ContactRepository
9 +import fr.ebii.card2vcf.data.toCard
10 +import kotlinx.coroutines.flow.MutableStateFlow
11 +import kotlinx.coroutines.flow.StateFlow
12 +import kotlinx.coroutines.flow.asStateFlow
13 +import kotlinx.coroutines.launch
14 +
15 +class ContactEditViewModel(
16 + private val repository: ContactRepository,
17 + val contactId: Long,
18 +) : ViewModel() {
19 +
20 + private val _card = MutableStateFlow(ContactCard())
21 + val card: StateFlow<ContactCard> = _card.asStateFlow()
22 +
23 + private val _profilePreview = MutableStateFlow<Bitmap?>(null)
24 + val profilePreview: StateFlow<Bitmap?> = _profilePreview.asStateFlow()
25 +
26 + private val _loaded = MutableStateFlow(false)
27 + val loaded: StateFlow<Boolean> = _loaded.asStateFlow()
28 +
29 + private val _saveDone = MutableStateFlow(false)
30 + val saveDone: StateFlow<Boolean> = _saveDone.asStateFlow()
31 +
32 + private var pendingProfile: Bitmap? = null
33 +
34 + init {
35 + viewModelScope.launch {
36 + val entity = repository.getById(contactId) ?: return@launch
37 + _card.value = entity.toCard()
38 + entity.profileImagePath?.takeIf { it.isNotBlank() }?.let { path ->
39 + _profilePreview.value = BitmapFactory.decodeFile(path)
40 + }
41 + _loaded.value = true
42 + }
43 + }
44 +
45 + fun updateCard(card: ContactCard) {
46 + _card.value = card
47 + }
48 +
49 + fun setProfileBitmap(bitmap: Bitmap) {
50 + pendingProfile = bitmap
51 + _profilePreview.value = bitmap
52 + }
53 +
54 + fun save() {
55 + viewModelScope.launch {
56 + repository.updateFromCard(
57 + id = contactId,
58 + card = _card.value,
59 + cardBitmap = null,
60 + profileBitmap = pendingProfile,
61 + )
62 + _saveDone.value = true
63 + }
64 + }
65 +}
M android/app/src/main/java/fr/ebii/card2vcf/ui/nav/Card2vcfNavHost.kt
+45 -5
@@ -4,7 +4,9 @@ import androidx.compose.foundation.layout.Box
4 4 import androidx.compose.foundation.layout.fillMaxSize
5 5 import androidx.compose.material3.Text
6 6 import androidx.compose.runtime.Composable
7 +import androidx.compose.runtime.LaunchedEffect
7 8 import androidx.compose.runtime.remember
9 +import androidx.compose.runtime.rememberCoroutineScope
8 10 import androidx.compose.ui.Alignment
9 11 import androidx.compose.ui.Modifier
10 12 import androidx.compose.ui.platform.LocalContext
@@ -23,8 +25,12 @@ import fr.ebii.card2vcf.ui.ScanCarteScreen
23 25 import fr.ebii.card2vcf.ui.ScanCarteViewModel
24 26 import fr.ebii.card2vcf.ui.carnet.CarnetScreen
25 27 import fr.ebii.card2vcf.ui.carnet.CarnetViewModel
28 +import fr.ebii.card2vcf.ui.contact.ContactEditScreen
29 +import fr.ebii.card2vcf.ui.contact.ContactEditViewModel
26 30 import fr.ebii.card2vcf.ui.contact.ContactPagerScreen
27 31 import fr.ebii.card2vcf.ui.contact.ContactPagerViewModel
32 +import fr.ebii.card2vcf.ui.contact.ManualContactScreen
33 +import kotlinx.coroutines.launch
28 34
29 35 object Routes {
30 36 const val Carnet = "carnet"
@@ -57,12 +63,14 @@ fun Card2vcfNavHost(
57 63 ) {
58 64 val carnetVm = remember(repository) { CarnetViewModel(repository) }
59 65 val context = LocalContext.current
60 - val scanVm = remember {
66 + val scanVm = remember(repository) {
61 67 ScanCarteViewModel(
62 68 scanEngine = OpenCvScanEngine(),
63 69 ocrEngine = TesseractOcrEngine(context.applicationContext),
70 + repository = repository,
64 71 )
65 72 }
73 + val scope = rememberCoroutineScope()
66 74
67 75 NavHost(
68 76 navController = navController,
@@ -114,8 +122,18 @@ fun Card2vcfNavHost(
114 122 navArgument("contactId") { type = NavType.LongType },
115 123 ),
116 124 ) { entry ->
117 - val id = entry.arguments?.getLong("contactId")
118 - PlaceholderRoute("Édition #$id")
125 + val id = entry.arguments?.getLong("contactId") ?: return@composable
126 + val editVm = remember(repository, id) {
127 + ContactEditViewModel(repository, id)
128 + }
129 + ContactEditScreen(
130 + viewModel = editVm,
131 + onBack = { navController.popBackStack() },
132 + onSaved = { navController.popBackStack() },
133 + onRescan = { contactId ->
134 + navController.navigate(Routes.scan(contactId))
135 + },
136 + )
119 137 }
120 138 composable(
121 139 route = Routes.Duplicates,
@@ -135,16 +153,38 @@ fun Card2vcfNavHost(
135 153 defaultValue = null
136 154 },
137 155 ),
138 - ) {
156 + ) { entry ->
157 + val editId = entry.arguments?.getString("editId")?.toLongOrNull()
158 + LaunchedEffect(editId) {
159 + scanVm.editingContactId = editId
160 + scanVm.reset()
161 + }
139 162 ScanCarteScreen(
140 163 viewModel = scanVm,
141 164 onCreateContact = onCreateContact,
142 165 onExportVcf = onExportVcf,
166 + onContactUpdated = { contactId ->
167 + scanVm.editingContactId = null
168 + scanVm.reset()
169 + navController.navigate(Routes.fiche(contactId)) {
170 + popUpTo(Routes.Carnet) { inclusive = false }
171 + }
172 + },
143 173 onBack = { navController.popBackStack() },
144 174 )
145 175 }
146 176 composable(Routes.Manual) {
147 - PlaceholderRoute("Saisie manuelle")
177 + ManualContactScreen(
178 + onBack = { navController.popBackStack() },
179 + onSave = { card ->
180 + scope.launch {
181 + val id = repository.insertFromCard(card, null, null)
182 + navController.navigate(Routes.fiche(id)) {
183 + popUpTo(Routes.Carnet) { inclusive = false }
184 + }
185 + }
186 + },
187 + )
148 188 }
149 189 composable(Routes.Draft) {
150 190 PlaceholderRoute("Brouillon")
M android/app/src/main/res/values-en/strings.xml
+10 -0
@@ -42,4 +42,14 @@
42 42 <string name="fiche_doublons">%d duplicate(s)</string>
43 43 <string name="fiche_photo_profil">Profile photo</string>
44 44 <string name="fiche_photo_carte">Card photo</string>
45 +
46 + <string name="edit_titre">Edit contact</string>
47 + <string name="edit_enregistrer">Save</string>
48 + <string name="edit_mettre_a_jour">Update</string>
49 + <string name="edit_rescanner">Rescan card</string>
50 + <string name="edit_photo_profil">Profile photo</string>
51 + <string name="edit_prendre_photo">Take photo</string>
52 + <string name="edit_choisir_photo">Choose photo</string>
53 + <string name="manual_titre">New contact</string>
54 + <string name="manual_sous_titre">Fill in the fields, then save to the address book.</string>
45 55 </resources>
M android/app/src/main/res/values/strings.xml
+10 -0
@@ -42,4 +42,14 @@
42 42 <string name="fiche_doublons">%d doublon(s)</string>
43 43 <string name="fiche_photo_profil">Photo de profil</string>
44 44 <string name="fiche_photo_carte">Photo de la carte</string>
45 +
46 + <string name="edit_titre">Modifier le contact</string>
47 + <string name="edit_enregistrer">Enregistrer</string>
48 + <string name="edit_mettre_a_jour">Mettre à jour</string>
49 + <string name="edit_rescanner">Rescanner la carte</string>
50 + <string name="edit_photo_profil">Photo de profil</string>
51 + <string name="edit_prendre_photo">Prendre une photo</string>
52 + <string name="edit_choisir_photo">Choisir une photo</string>
53 + <string name="manual_titre">Nouveau contact</string>
54 + <string name="manual_sous_titre">Saisissez les champs, puis enregistrez dans le carnet.</string>
45 55 </resources>
M android/app/src/main/res/xml/file_paths.xml
+1 -0
@@ -1,4 +1,5 @@
1 1 <?xml version="1.0" encoding="utf-8"?>
2 2 <paths>
3 3 <cache-path name="vcf" path="vcf/" />
4 + <cache-path name="images" path="images/" />
4 5 </paths>