carnet contact copie edition recherche swipe

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

9c721e05afdfd385e2612b56c85d8fd35059200e

1 parent(s)

25 fichiers modifiés +200 -120
M android/app/src/main/java/fr/ebii/card2vcf/data/ContactImageStore.kt
+7 -3
@@ -2,6 +2,7 @@ package fr.ebii.card2vcf.data
2 2
3 3 import android.content.Context
4 4 import android.graphics.Bitmap
5 +import fr.ebii.card2vcf.scan.ImageOrientation
5 6 import java.io.File
6 7 import java.io.FileOutputStream
7 8
@@ -19,12 +20,15 @@ class ContactImageStore(private val context: Context) {
19 20 dir(contactId).deleteRecursively()
20 21 }
21 22
23 + /**
24 + * Copie en ré-encodant après correction EXIF, pour que le fichier stocké
25 + * soit déjà « droit ».
26 + */
22 27 fun copyImage(fromPath: String?, toContactId: Long, name: String): String? {
23 28 if (fromPath.isNullOrBlank()) return null
24 29 val src = File(fromPath)
25 30 if (!src.isFile) return null
26 - val dest = File(dir(toContactId), name)
27 - src.copyTo(dest, overwrite = true)
28 - return dest.absolutePath
31 + val oriented = ImageOrientation.fromFile(src)?.bitmap ?: return null
32 + return saveJpeg(toContactId, name, oriented)
29 33 }
30 34 }
M android/app/src/main/java/fr/ebii/card2vcf/data/ContactRepository.kt
+19 -0
@@ -1,10 +1,13 @@
1 1 package fr.ebii.card2vcf.data
2 2
3 3 import android.graphics.Bitmap
4 +import android.graphics.Matrix
4 5 import fr.ebii.card2vcf.contact.ContactCard
5 6 import fr.ebii.card2vcf.crm.ContactMerge
6 7 import fr.ebii.card2vcf.crm.ContactNormalizer
7 8 import fr.ebii.card2vcf.crm.MergeFieldChoices
9 +import fr.ebii.card2vcf.scan.ImageOrientation
10 +import java.io.File
8 11 import kotlinx.coroutines.flow.Flow
9 12
10 13 data class VcfImportResult(
@@ -90,6 +93,22 @@ class ContactRepository(
90 93 images.deleteAll(id)
91 94 }
92 95
96 + /** Tourne la photo carte de [degrees] (ex. -90 = 90° à gauche) et réécrit card.jpg. */
97 + suspend fun rotateCardImage(id: Long, degrees: Int = -90) {
98 + val existing = dao.getById(id) ?: return
99 + val path = existing.cardImagePath?.takeIf { it.isNotBlank() } ?: return
100 + val src = ImageOrientation.fromFile(File(path))?.bitmap ?: return
101 + val matrix = Matrix().apply { postRotate(degrees.toFloat()) }
102 + val rotated = Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true)
103 + val newPath = images.saveJpeg(id, "card.jpg", rotated)
104 + dao.update(
105 + existing.copy(
106 + cardImagePath = newPath,
107 + updatedAt = System.currentTimeMillis(),
108 + ),
109 + )
110 + }
111 +
93 112 suspend fun markDifferent(a: Long, b: Long) {
94 113 val lo = minOf(a, b)
95 114 val hi = maxOf(a, b)
M android/app/src/main/java/fr/ebii/card2vcf/ui/ContactDraftScreen.kt
+8 -1
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
10 10 import androidx.compose.foundation.layout.padding
11 11 import androidx.compose.foundation.rememberScrollState
12 12 import androidx.compose.foundation.shape.RoundedCornerShape
13 +import androidx.compose.foundation.text.selection.SelectionContainer
13 14 import androidx.compose.foundation.verticalScroll
14 15 import androidx.compose.material3.Button
15 16 import androidx.compose.material3.ButtonDefaults
@@ -61,7 +62,13 @@ fun ContactDraftScreen(
61 62 Modifier.fillMaxWidth().background(Surface)
62 63 .border(1.dp, Bordure).padding(12.dp),
63 64 ) {
64 - Text(draft.rawOcrText, fontFamily = FontFamily.Monospace, color = TexteFaible)
65 + SelectionContainer {
66 + Text(
67 + draft.rawOcrText,
68 + fontFamily = FontFamily.Monospace,
69 + color = TexteFaible,
70 + )
71 + }
65 72 }
66 73 }
67 74 Button(
M android/app/src/main/java/fr/ebii/card2vcf/ui/carnet/CarnetScreen.kt
+57 -31
@@ -2,6 +2,11 @@ package fr.ebii.card2vcf.ui.carnet
2 2
3 3 import androidx.compose.foundation.background
4 4 import androidx.compose.foundation.clickable
5 +import androidx.compose.foundation.BorderStroke
6 +import androidx.compose.foundation.background
7 +import androidx.compose.foundation.border
8 +import androidx.compose.foundation.clickable
9 +import androidx.compose.foundation.layout.Arrangement
5 10 import androidx.compose.foundation.layout.Box
6 11 import androidx.compose.foundation.layout.Column
7 12 import androidx.compose.foundation.layout.Row
@@ -17,15 +22,18 @@ import androidx.compose.foundation.lazy.rememberLazyListState
17 22 import androidx.compose.foundation.shape.RoundedCornerShape
18 23 import androidx.compose.material.icons.Icons
19 24 import androidx.compose.material.icons.filled.Add
25 +import androidx.compose.material.icons.filled.PhotoCamera
20 26 import androidx.compose.material3.DropdownMenu
21 27 import androidx.compose.material3.DropdownMenuItem
22 28 import androidx.compose.material3.FloatingActionButton
29 +import androidx.compose.material3.FloatingActionButtonDefaults
23 30 import androidx.compose.material3.HorizontalDivider
24 31 import androidx.compose.material3.Icon
25 32 import androidx.compose.material3.MaterialTheme
26 33 import androidx.compose.material3.OutlinedTextField
27 34 import androidx.compose.material3.OutlinedTextFieldDefaults
28 35 import androidx.compose.material3.Scaffold
36 +import androidx.compose.material3.SmallFloatingActionButton
29 37 import androidx.compose.material3.Text
30 38 import androidx.compose.material3.TextButton
31 39 import androidx.compose.runtime.Composable
@@ -76,43 +84,61 @@ fun CarnetScreen(
76 84 modifier = modifier.fillMaxSize().background(Fond),
77 85 containerColor = Fond,
78 86 floatingActionButton = {
79 - Box {
87 + Column(
88 + horizontalAlignment = Alignment.End,
89 + verticalArrangement = Arrangement.spacedBy(12.dp),
90 + ) {
91 + Box {
92 + SmallFloatingActionButton(
93 + onClick = { fabMenuOpen = true },
94 + shape = Square,
95 + containerColor = Fond,
96 + contentColor = Ink,
97 + elevation = FloatingActionButtonDefaults.elevation(
98 + defaultElevation = 0.dp,
99 + pressedElevation = 0.dp,
100 + ),
101 + modifier = Modifier.border(BorderStroke(1.dp, Bordure), Square),
102 + ) {
103 + Icon(
104 + Icons.Filled.Add,
105 + contentDescription = stringResource(R.string.carnet_fab_ajouter),
106 + )
107 + }
108 + DropdownMenu(
109 + expanded = fabMenuOpen,
110 + onDismissRequest = { fabMenuOpen = false },
111 + shape = Square,
112 + ) {
113 + DropdownMenuItem(
114 + text = { Text(stringResource(R.string.carnet_fab_manuel)) },
115 + onClick = {
116 + fabMenuOpen = false
117 + onManual()
118 + },
119 + )
120 + DropdownMenuItem(
121 + text = { Text(stringResource(R.string.carnet_fab_import)) },
122 + onClick = {
123 + fabMenuOpen = false
124 + onImportVcf()
125 + },
126 + )
127 + }
128 + }
80 129 FloatingActionButton(
81 - onClick = { fabMenuOpen = true },
130 + onClick = onScan,
82 131 shape = Square,
83 132 containerColor = Ink,
84 133 contentColor = OnPrimary,
134 + elevation = FloatingActionButtonDefaults.elevation(
135 + defaultElevation = 0.dp,
136 + pressedElevation = 0.dp,
137 + ),
85 138 ) {
86 139 Icon(
87 - Icons.Filled.Add,
88 - contentDescription = stringResource(R.string.carnet_fab_ajouter),
89 - )
90 - }
91 - DropdownMenu(
92 - expanded = fabMenuOpen,
93 - onDismissRequest = { fabMenuOpen = false },
94 - shape = Square,
95 - ) {
96 - DropdownMenuItem(
97 - text = { Text(stringResource(R.string.carnet_fab_scanner)) },
98 - onClick = {
99 - fabMenuOpen = false
100 - onScan()
101 - },
102 - )
103 - DropdownMenuItem(
104 - text = { Text(stringResource(R.string.carnet_fab_manuel)) },
105 - onClick = {
106 - fabMenuOpen = false
107 - onManual()
108 - },
109 - )
110 - DropdownMenuItem(
111 - text = { Text(stringResource(R.string.carnet_fab_import)) },
112 - onClick = {
113 - fabMenuOpen = false
114 - onImportVcf()
115 - },
140 + Icons.Filled.PhotoCamera,
141 + contentDescription = stringResource(R.string.carnet_fab_scanner),
116 142 )
117 143 }
118 144 }
M android/app/src/main/java/fr/ebii/card2vcf/ui/contact/ContactEditScreen.kt
+3 -3
@@ -1,6 +1,5 @@
1 1 package fr.ebii.card2vcf.ui.contact
2 2
3 -import android.graphics.BitmapFactory
4 3 import android.net.Uri
5 4 import androidx.activity.compose.rememberLauncherForActivityResult
6 5 import androidx.activity.result.contract.ActivityResultContracts
@@ -47,6 +46,7 @@ import androidx.compose.ui.unit.dp
47 46 import androidx.core.content.FileProvider
48 47 import fr.ebii.card2vcf.R
49 48 import fr.ebii.card2vcf.contact.ContactCard
49 +import fr.ebii.card2vcf.scan.ImageOrientation
50 50 import fr.ebii.card2vcf.ui.ContactDraftFields
51 51 import fr.ebii.card2vcf.ui.theme.Bordure
52 52 import fr.ebii.card2vcf.ui.theme.Encre
@@ -85,7 +85,7 @@ fun ContactEditScreen(
85 85 if (!success) return@rememberLauncherForActivityResult
86 86 val uri = pendingCaptureUri ?: return@rememberLauncherForActivityResult
87 87 context.contentResolver.openInputStream(uri)?.use { stream ->
88 - BitmapFactory.decodeStream(stream)?.let(viewModel::setProfileBitmap)
88 + ImageOrientation.fromStream(stream)?.bitmap?.let(viewModel::setProfileBitmap)
89 89 }
90 90 }
91 91
@@ -94,7 +94,7 @@ fun ContactEditScreen(
94 94 ) { uri ->
95 95 uri ?: return@rememberLauncherForActivityResult
96 96 context.contentResolver.openInputStream(uri)?.use { stream ->
97 - BitmapFactory.decodeStream(stream)?.let(viewModel::setProfileBitmap)
97 + ImageOrientation.fromStream(stream)?.bitmap?.let(viewModel::setProfileBitmap)
98 98 }
99 99 }
100 100
M android/app/src/main/java/fr/ebii/card2vcf/ui/contact/ContactEditViewModel.kt
+3 -2
@@ -1,12 +1,13 @@
1 1 package fr.ebii.card2vcf.ui.contact
2 2
3 3 import android.graphics.Bitmap
4 -import android.graphics.BitmapFactory
5 4 import androidx.lifecycle.ViewModel
6 5 import androidx.lifecycle.viewModelScope
7 6 import fr.ebii.card2vcf.contact.ContactCard
8 7 import fr.ebii.card2vcf.data.ContactRepository
9 8 import fr.ebii.card2vcf.data.toCard
9 +import fr.ebii.card2vcf.scan.ImageOrientation
10 +import java.io.File
10 11 import kotlinx.coroutines.flow.MutableStateFlow
11 12 import kotlinx.coroutines.flow.StateFlow
12 13 import kotlinx.coroutines.flow.asStateFlow
@@ -36,7 +37,7 @@ class ContactEditViewModel(
36 37 val entity = repository.getById(contactId) ?: return@launch
37 38 _card.value = entity.toCard()
38 39 entity.profileImagePath?.takeIf { it.isNotBlank() }?.let { path ->
39 - _profilePreview.value = BitmapFactory.decodeFile(path)
40 + _profilePreview.value = ImageOrientation.fromFile(File(path))?.bitmap
40 41 }
41 42 _loaded.value = true
42 43 }
M android/app/src/main/java/fr/ebii/card2vcf/ui/contact/ContactPagerScreen.kt
+72 -70
@@ -2,12 +2,12 @@ package fr.ebii.card2vcf.ui.contact
2 2
3 3 import android.content.ActivityNotFoundException
4 4 import android.content.Intent
5 -import android.graphics.BitmapFactory
6 5 import android.net.Uri
6 +import java.io.File
7 7 import androidx.compose.foundation.Image
8 8 import androidx.compose.foundation.background
9 9 import androidx.compose.foundation.border
10 -import androidx.compose.foundation.gestures.detectTapGestures
10 +import androidx.compose.foundation.clickable
11 11 import androidx.compose.foundation.layout.Arrangement
12 12 import androidx.compose.foundation.layout.Box
13 13 import androidx.compose.foundation.layout.Column
@@ -25,39 +25,38 @@ import androidx.compose.foundation.shape.RoundedCornerShape
25 25 import androidx.compose.foundation.verticalScroll
26 26 import androidx.compose.material.icons.Icons
27 27 import androidx.compose.material.icons.automirrored.outlined.ArrowBack
28 +import androidx.compose.material.icons.automirrored.outlined.RotateLeft
28 29 import androidx.compose.material3.HorizontalDivider
29 30 import androidx.compose.material3.Icon
30 31 import androidx.compose.material3.IconButton
31 32 import androidx.compose.material3.MaterialTheme
33 +import androidx.compose.material3.OutlinedButton
32 34 import androidx.compose.material3.Text
33 35 import androidx.compose.material3.TextButton
34 36 import androidx.compose.runtime.Composable
35 37 import androidx.compose.runtime.collectAsState
36 38 import androidx.compose.runtime.getValue
37 39 import androidx.compose.runtime.remember
38 -import androidx.compose.runtime.rememberCoroutineScope
39 40 import androidx.compose.ui.Alignment
40 41 import androidx.compose.ui.Modifier
41 42 import androidx.compose.ui.graphics.ImageBitmap
42 43 import androidx.compose.ui.graphics.asImageBitmap
43 -import androidx.compose.ui.input.pointer.pointerInput
44 44 import androidx.compose.ui.layout.ContentScale
45 45 import androidx.compose.ui.platform.LocalContext
46 46 import androidx.compose.ui.res.stringResource
47 47 import androidx.compose.ui.unit.dp
48 48 import fr.ebii.card2vcf.R
49 49 import fr.ebii.card2vcf.data.CrmContactEntity
50 +import fr.ebii.card2vcf.scan.ImageOrientation
50 51 import fr.ebii.card2vcf.ui.theme.Bordure
51 52 import fr.ebii.card2vcf.ui.theme.Encre
52 53 import fr.ebii.card2vcf.ui.theme.Fond
54 +import fr.ebii.card2vcf.ui.theme.Ink
55 +import fr.ebii.card2vcf.ui.theme.Link
53 56 import fr.ebii.card2vcf.ui.theme.Surface
54 57 import fr.ebii.card2vcf.ui.theme.TexteFaible
55 -import kotlinx.coroutines.Job
56 -import kotlinx.coroutines.delay
57 -import kotlinx.coroutines.launch
58 58
59 59 private val Square = RoundedCornerShape(0.dp)
60 -private const val TapTimeoutMs = 400L
61 60
62 61 @Composable
63 62 fun ContactPagerScreen(
@@ -114,7 +113,10 @@ fun ContactPagerScreen(
114 113 state = pagerState,
115 114 modifier = Modifier.fillMaxSize(),
116 115 ) { page ->
117 - ContactFichePage(contact = contacts[page])
116 + ContactFichePage(
117 + contact = contacts[page],
118 + onRotateCardLeft = { viewModel.rotateCardLeft(contacts[page].id) },
119 + )
118 120 }
119 121 }
120 122 }
@@ -159,25 +161,28 @@ private fun FicheTopBar(
159 161 @Composable
160 162 private fun ContactFichePage(
161 163 contact: CrmContactEntity,
164 + onRotateCardLeft: () -> Unit,
162 165 modifier: Modifier = Modifier,
163 166 ) {
164 167 val context = LocalContext.current
165 - val scope = rememberCoroutineScope()
166 - val tracker = remember(contact.id) { TapGestureTracker() }
168 + val phones = remember(contact.phones) { contact.phones.filter { it.isNotBlank() } }
169 + val emails = remember(contact.emails) { contact.emails.filter { it.isNotBlank() } }
167 170
168 - fun openEmail() {
169 - val email = contact.emails.firstOrNull()?.takeIf { it.isNotBlank() } ?: return
171 + fun openEmail(email: String) {
170 172 try {
171 - context.startActivity(Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:$email")))
173 + context.startActivity(
174 + Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:${Uri.encode(email)}")),
175 + )
172 176 } catch (_: ActivityNotFoundException) {
173 177 // no-op
174 178 }
175 179 }
176 180
177 - fun openDial() {
178 - val phone = contact.phones.firstOrNull()?.takeIf { it.isNotBlank() } ?: return
181 + fun openDial(phone: String) {
179 182 try {
180 - context.startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:$phone")))
183 + val tel = phone.filter { it.isDigit() || it == '+' }
184 + if (tel.isEmpty()) return
185 + context.startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:$tel")))
181 186 } catch (_: ActivityNotFoundException) {
182 187 // no-op
183 188 }
@@ -186,23 +191,13 @@ private fun ContactFichePage(
186 191 Column(
187 192 modifier
188 193 .fillMaxSize()
189 - .pointerInput(contact.id, contact.emails, contact.phones) {
190 - // onDoubleTap → mailto (différé 400 ms) ; 3ᵉ tap dans la fenêtre → dial.
191 - detectTapGestures(
192 - onDoubleTap = {
193 - tracker.onDoubleTapArm(scope = scope, onDouble = { openEmail() })
194 - },
195 - onTap = {
196 - tracker.onTap(onTriple = { openDial() })
197 - },
198 - )
199 - }
200 194 .verticalScroll(rememberScrollState())
201 195 .padding(horizontal = 18.dp, vertical = 12.dp),
202 196 verticalArrangement = Arrangement.spacedBy(12.dp),
203 197 ) {
204 198 PathImage(
205 199 path = contact.profileImagePath,
200 + revision = contact.updatedAt,
206 201 contentDescription = stringResource(R.string.fiche_photo_profil),
207 202 modifier = Modifier
208 203 .size(96.dp)
@@ -219,13 +214,15 @@ private fun ContactFichePage(
219 214 FieldBlock(stringResource(R.string.scan_champ_nom_famille), contact.lastName)
220 215 FieldBlock(stringResource(R.string.scan_champ_societe), contact.company)
221 216 FieldBlock(stringResource(R.string.scan_champ_poste), contact.jobTitle)
222 - FieldBlock(
223 - stringResource(R.string.scan_champ_tel),
224 - contact.phones.filter { it.isNotBlank() }.joinToString("\n").ifBlank { null },
217 + ClickableFieldBlock(
218 + label = stringResource(R.string.scan_champ_tel),
219 + values = phones,
220 + onClick = ::openDial,
225 221 )
226 - FieldBlock(
227 - stringResource(R.string.scan_champ_email),
228 - contact.emails.filter { it.isNotBlank() }.joinToString("\n").ifBlank { null },
222 + ClickableFieldBlock(
223 + label = stringResource(R.string.scan_champ_email),
224 + values = emails,
225 + onClick = ::openEmail,
229 226 )
230 227 FieldBlock(stringResource(R.string.scan_champ_site), contact.website)
231 228 FieldBlock(stringResource(R.string.scan_champ_adresse), contact.address)
@@ -234,13 +231,27 @@ private fun ContactFichePage(
234 231 HorizontalDivider(color = Bordure, thickness = 1.dp)
235 232 PathImage(
236 233 path = contact.cardImagePath,
234 + revision = contact.updatedAt,
237 235 contentDescription = stringResource(R.string.fiche_photo_carte),
238 236 modifier = Modifier
239 237 .fillMaxWidth()
240 - .height(200.dp)
238 + .height(220.dp)
241 239 .border(1.dp, Bordure, Square),
242 240 contentScale = ContentScale.Fit,
243 241 )
242 + OutlinedButton(
243 + onClick = onRotateCardLeft,
244 + shape = Square,
245 + modifier = Modifier.fillMaxWidth(),
246 + ) {
247 + Icon(
248 + Icons.AutoMirrored.Outlined.RotateLeft,
249 + contentDescription = null,
250 + tint = Ink,
251 + )
252 + Spacer(Modifier.size(8.dp))
253 + Text(stringResource(R.string.fiche_tourner_gauche), color = Ink)
254 + }
244 255 }
245 256
246 257 if (!contact.notes.isNullOrBlank()) {
@@ -250,39 +261,6 @@ private fun ContactFichePage(
250 261 }
251 262 }
252 263
253 -/**
254 - * Compteur pour triple-tap (timeout [TapTimeoutMs]).
255 - * Le double-tap mail reste sur [detectTapGestures] `onDoubleTap`.
256 - *
257 - * Avec `onDoubleTap` présent, Compose n'émet `onTap` que pour les taps isolés ;
258 - * pour un vrai 1-2-3 rapide on compte aussi via [onDoubleTapSeen] :
259 - * double → arme une fenêtre, 3ᵉ tap (`onTap` suivant) → dial et annule le mail.
260 - */
261 -private class TapGestureTracker {
262 - private var armedForTriple = false
263 - private var job: Job? = null
264 -
265 - fun onDoubleTapArm(scope: kotlinx.coroutines.CoroutineScope, onDouble: () -> Unit) {
266 - armedForTriple = true
267 - job?.cancel()
268 - job = scope.launch {
269 - delay(TapTimeoutMs)
270 - if (armedForTriple) {
271 - armedForTriple = false
272 - onDouble()
273 - }
274 - }
275 - }
276 -
277 - fun onTap(onTriple: () -> Unit) {
278 - if (armedForTriple) {
279 - armedForTriple = false
280 - job?.cancel()
281 - onTriple()
282 - }
283 - }
284 -}
285 -
286 264 @Composable
287 265 private fun FieldBlock(label: String, value: String?) {
288 266 if (value.isNullOrBlank()) return
@@ -293,15 +271,39 @@ private fun FieldBlock(label: String, value: String?) {
293 271 }
294 272
295 273 @Composable
274 +private fun ClickableFieldBlock(
275 + label: String,
276 + values: List<String>,
277 + onClick: (String) -> Unit,
278 +) {
279 + if (values.isEmpty()) return
280 + Column(Modifier.fillMaxWidth()) {
281 + Text(label, style = MaterialTheme.typography.labelMedium, color = TexteFaible)
282 + values.forEach { value ->
283 + Text(
284 + text = value,
285 + style = MaterialTheme.typography.bodyLarge,
286 + color = Link,
287 + modifier = Modifier
288 + .fillMaxWidth()
289 + .clickable { onClick(value) }
290 + .padding(vertical = 4.dp),
291 + )
292 + }
293 + }
294 +}
295 +
296 +@Composable
296 297 private fun PathImage(
297 298 path: String?,
298 299 contentDescription: String,
299 300 modifier: Modifier = Modifier,
300 301 contentScale: ContentScale = ContentScale.Crop,
302 + revision: Long = 0L,
301 303 ) {
302 - val bitmap: ImageBitmap? = remember(path) {
304 + val bitmap: ImageBitmap? = remember(path, revision) {
303 305 path?.takeIf { it.isNotBlank() }?.let { p ->
304 - BitmapFactory.decodeFile(p)?.asImageBitmap()
306 + ImageOrientation.fromFile(File(p))?.bitmap?.asImageBitmap()
305 307 }
306 308 }
307 309 if (bitmap != null) {
M android/app/src/main/java/fr/ebii/card2vcf/ui/contact/ContactPagerViewModel.kt
+9 -1
@@ -13,10 +13,11 @@ import kotlinx.coroutines.flow.StateFlow
13 13 import kotlinx.coroutines.flow.map
14 14 import kotlinx.coroutines.flow.mapLatest
15 15 import kotlinx.coroutines.flow.stateIn
16 +import kotlinx.coroutines.launch
16 17
17 18 @OptIn(ExperimentalCoroutinesApi::class)
18 19 class ContactPagerViewModel(
19 - repository: ContactRepository,
20 + private val repository: ContactRepository,
20 21 val initialContactId: Long,
21 22 val sort: ContactSort,
22 23 ) : ViewModel() {
@@ -39,4 +40,11 @@ class ContactPagerViewModel(
39 40 val idx = list.indexOfFirst { it.id == initialContactId }
40 41 return if (idx >= 0) idx else 0
41 42 }
43 +
44 + /** 90° à gauche (sens antihoraire). */
45 + fun rotateCardLeft(contactId: Long) {
46 + viewModelScope.launch {
47 + repository.rotateCardImage(contactId, degrees = -90)
48 + }
49 + }
42 50 }
M android/app/src/main/java/fr/ebii/card2vcf/ui/contact/DuplicateReviewScreen.kt
+3 -2
@@ -1,6 +1,5 @@
1 1 package fr.ebii.card2vcf.ui.contact
2 2
3 -import android.graphics.BitmapFactory
4 3 import androidx.compose.foundation.Image
5 4 import androidx.compose.foundation.background
6 5 import androidx.compose.foundation.border
@@ -51,6 +50,7 @@ import fr.ebii.card2vcf.R
51 50 import fr.ebii.card2vcf.crm.MergeFieldChoices
52 51 import fr.ebii.card2vcf.crm.NotesMode
53 52 import fr.ebii.card2vcf.data.CrmContactEntity
53 +import fr.ebii.card2vcf.scan.ImageOrientation
54 54 import fr.ebii.card2vcf.ui.theme.Bordure
55 55 import fr.ebii.card2vcf.ui.theme.Encre
56 56 import fr.ebii.card2vcf.ui.theme.Fond
@@ -58,6 +58,7 @@ import fr.ebii.card2vcf.ui.theme.Ink
58 58 import fr.ebii.card2vcf.ui.theme.OnPrimary
59 59 import fr.ebii.card2vcf.ui.theme.Surface
60 60 import fr.ebii.card2vcf.ui.theme.TexteFaible
61 +import java.io.File
61 62
62 63 private val Square = RoundedCornerShape(0.dp)
63 64
@@ -427,7 +428,7 @@ private fun ImagePicker(
427 428 )
428 429 val path = pathOf(c)
429 430 val bitmap = remember(path) {
430 - path?.let { BitmapFactory.decodeFile(it)?.asImageBitmap() }
431 + path?.let { ImageOrientation.fromFile(File(it))?.bitmap?.asImageBitmap() }
431 432 }
432 433 if (bitmap != null) {
433 434 Image(
M android/app/src/main/res/drawable/ic_launcher_foreground.png
+0 -0
A android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+5 -0
@@ -0,0 +1,5 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3 + <background android:drawable="@color/ic_launcher_background"/>
4 + <foreground android:drawable="@drawable/ic_launcher_foreground"/>
5 +</adaptive-icon>
A android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+5 -0
@@ -0,0 +1,5 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3 + <background android:drawable="@color/ic_launcher_background"/>
4 + <foreground android:drawable="@drawable/ic_launcher_foreground"/>
5 +</adaptive-icon>
M android/app/src/main/res/mipmap-hdpi/ic_launcher.png
+0 -0
M android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
+0 -0
M android/app/src/main/res/mipmap-mdpi/ic_launcher.png
+0 -0
M android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
+0 -0
M android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
+0 -0
M android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
+0 -0
M android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
+0 -0
M android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
+0 -0
M android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
+0 -0
M android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
+0 -0
M android/app/src/main/res/values-en/strings.xml
+4 -3
@@ -33,9 +33,9 @@
33 33 <string name="carnet_tri_prenom">First name</string>
34 34 <string name="carnet_tri_entreprise">Company</string>
35 35 <string name="carnet_tri_date">Date</string>
36 - <string name="carnet_fab_ajouter">Add contact</string>
37 - <string name="carnet_fab_scanner">Scan</string>
38 - <string name="carnet_fab_manuel">Manual</string>
36 + <string name="carnet_fab_ajouter">Other add options</string>
37 + <string name="carnet_fab_scanner">Scan a card</string>
38 + <string name="carnet_fab_manuel">Manual entry</string>
39 39 <string name="carnet_fab_import">Import VCF</string>
40 40 <string name="carnet_vide">No contacts — scan a card or import a VCF</string>
41 41
@@ -43,6 +43,7 @@
43 43 <string name="fiche_doublons">%d duplicate(s)</string>
44 44 <string name="fiche_photo_profil">Profile photo</string>
45 45 <string name="fiche_photo_carte">Card photo</string>
46 + <string name="fiche_tourner_gauche">Rotate 90° left</string>
46 47
47 48 <string name="edit_titre">Edit contact</string>
48 49 <string name="edit_enregistrer">Save</string>
M android/app/src/main/res/values/colors.xml
+1 -1
@@ -1,4 +1,4 @@
1 1 <?xml version="1.0" encoding="utf-8"?>
2 2 <resources>
3 - <color name="ic_launcher_background">#000000</color>
3 + <color name="ic_launcher_background">#9aceec</color>
4 4 </resources>
M android/app/src/main/res/values/strings.xml
+4 -3
@@ -33,9 +33,9 @@
33 33 <string name="carnet_tri_prenom">Prénom</string>
34 34 <string name="carnet_tri_entreprise">Entreprise</string>
35 35 <string name="carnet_tri_date">Date</string>
36 - <string name="carnet_fab_ajouter">Ajouter un contact</string>
37 - <string name="carnet_fab_scanner">Scanner</string>
38 - <string name="carnet_fab_manuel">Manuel</string>
36 + <string name="carnet_fab_ajouter">Autres ajouts</string>
37 + <string name="carnet_fab_scanner">Scanner une carte</string>
38 + <string name="carnet_fab_manuel">Saisie manuelle</string>
39 39 <string name="carnet_fab_import">Import VCF</string>
40 40 <string name="carnet_vide">Aucun contact — scanner une carte ou importer un VCF</string>
41 41
@@ -43,6 +43,7 @@
43 43 <string name="fiche_doublons">%d doublon(s)</string>
44 44 <string name="fiche_photo_profil">Photo de profil</string>
45 45 <string name="fiche_photo_carte">Photo de la carte</string>
46 + <string name="fiche_tourner_gauche">Tourner 90° à gauche</string>
46 47
47 48 <string name="edit_titre">Modifier le contact</string>
48 49 <string name="edit_enregistrer">Enregistrer</string>