v1.4 : groupes de destinataires et simplification de l'écran principal

- Nouveau modèle : groupes nommés (famille, amis…) stockés en JSON dans les
  préférences, avec migration automatique de l'ancienne liste de numéros
  vers un groupe « Mes contacts »
- Écran principal : la roue crantée (doublon) disparaît ; la carte du bas
  affiche des puces de sélection du groupe actif + ses numéros, et ouvre
  la gestion des groupes
- Paramètres : nom (enregistré à la saisie) + liste des groupes ; création,
  édition et suppression via boîte de dialogue avec validation des numéros
- Prefs.kt : accès centralisé aux préférences

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cosano-star 2026-07-13 14:45:19 +02:00
parent c7a753627b
commit 191c4158c7
9 changed files with 416 additions and 139 deletions

View file

@ -23,8 +23,8 @@ android {
applicationId = "com.example.maposition" applicationId = "com.example.maposition"
minSdk = 21 minSdk = 21
targetSdk = 36 targetSdk = 36
versionCode = 4 versionCode = 5
versionName = "1.3" versionName = "1.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View file

@ -17,7 +17,6 @@ import android.os.Looper
import android.os.SystemClock import android.os.SystemClock
import android.telephony.SmsManager import android.telephony.SmsManager
import android.view.View import android.view.View
import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
@ -27,6 +26,8 @@ import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority import com.google.android.gms.location.Priority
import com.google.android.gms.tasks.CancellationTokenSource import com.google.android.gms.tasks.CancellationTokenSource
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.progressindicator.CircularProgressIndicator import com.google.android.material.progressindicator.CircularProgressIndicator
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -52,6 +53,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var statusText: TextView private lateinit var statusText: TextView
private lateinit var progress: CircularProgressIndicator private lateinit var progress: CircularProgressIndicator
private lateinit var recipientsText: TextView private lateinit var recipientsText: TextView
private lateinit var groupChips: ChipGroup
// État d'un envoi en cours // État d'un envoi en cours
private var busy = false private var busy = false
@ -83,6 +85,7 @@ class MainActivity : AppCompatActivity() {
statusText = findViewById(R.id.statusText) statusText = findViewById(R.id.statusText)
progress = findViewById(R.id.progress) progress = findViewById(R.id.progress)
recipientsText = findViewById(R.id.recipientsText) recipientsText = findViewById(R.id.recipientsText)
groupChips = findViewById(R.id.groupChips)
ContextCompat.registerReceiver( ContextCompat.registerReceiver(
this, smsSentReceiver, IntentFilter(ACTION_SMS_SENT), this, smsSentReceiver, IntentFilter(ACTION_SMS_SENT),
@ -90,7 +93,6 @@ class MainActivity : AppCompatActivity() {
) )
sendButton.setOnClickListener { startSendFlow() } sendButton.setOnClickListener { startSendFlow() }
findViewById<ImageButton>(R.id.settingsButton).setOnClickListener { openSettings() }
findViewById<MaterialCardView>(R.id.recipientsCard).setOnClickListener { openSettings() } findViewById<MaterialCardView>(R.id.recipientsCard).setOnClickListener { openSettings() }
} }
@ -109,28 +111,41 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, SettingsActivity::class.java)) startActivity(Intent(this, SettingsActivity::class.java))
} }
private fun getSavedSettings(): Pair<String, List<String>> { // --- Carte des groupes de destinataires ---
val prefs = getSharedPreferences("MaPositionPrefs", MODE_PRIVATE)
val name = prefs.getString("name", "") ?: ""
val phones = prefs.getString("phones", "") ?: ""
val phoneList = phones.split(";").map { it.trim() }.filter { it.isNotEmpty() }
return Pair(name, phoneList)
}
private fun refreshRecipientsCard() { private fun refreshRecipientsCard() {
val (name, phones) = getSavedSettings() val groups = Prefs.getGroups(this)
recipientsText.text = if (phones.isEmpty()) { groupChips.removeAllViews()
getString(R.string.recipients_none)
} else { if (groups.isEmpty()) {
val who = if (name.isBlank()) "" else "$name" groupChips.visibility = View.GONE
who + phones.joinToString(", ") recipientsText.text = getString(R.string.recipients_none)
return
} }
groupChips.visibility = View.VISIBLE
val active = Prefs.getActiveGroup(this)!!
groups.forEach { g ->
val chip = Chip(this).apply {
text = g.name
isCheckable = true
isChecked = g.name == active.name
setOnClickListener {
Prefs.setActiveGroupName(this@MainActivity, g.name)
refreshRecipientsCard()
}
}
groupChips.addView(chip)
}
recipientsText.text = active.phones.joinToString(", ")
} }
// --- Déclenchement de l'envoi ---
private fun startSendFlow() { private fun startSendFlow() {
if (busy) return if (busy) return
val (_, phones) = getSavedSettings() val active = Prefs.getActiveGroup(this)
if (phones.isEmpty()) { if (active == null || active.phones.isEmpty()) {
setStatus(getString(R.string.status_no_recipients)) setStatus(getString(R.string.status_no_recipients))
openSettings() openSettings()
return return
@ -220,7 +235,12 @@ class MainActivity : AppCompatActivity() {
} }
private fun sendSmsWithLocation(location: Location, stale: Boolean) { private fun sendSmsWithLocation(location: Location, stale: Boolean) {
val (name, phones) = getSavedSettings() val name = Prefs.getName(this)
val phones = Prefs.getActiveGroup(this)?.phones ?: emptyList()
if (phones.isEmpty()) {
abort(getString(R.string.status_no_recipients))
return
}
val link = "https://maps.google.com/?q=${location.latitude},${location.longitude}" val link = "https://maps.google.com/?q=${location.latitude},${location.longitude}"
val ageMin = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - location.time) val ageMin = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - location.time)

View file

@ -0,0 +1,64 @@
package com.example.maposition
import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
data class Group(val name: String, val phones: List<String>)
object Prefs {
private const val FILE = "MaPositionPrefs"
private fun sp(ctx: Context) = ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
fun getName(ctx: Context): String = sp(ctx).getString("name", "") ?: ""
fun setName(ctx: Context, name: String) {
sp(ctx).edit().putString("name", name.trim()).apply()
}
fun getGroups(ctx: Context): MutableList<Group> {
val raw = sp(ctx).getString("groups", null)
val groups = mutableListOf<Group>()
if (raw != null) {
val arr = JSONArray(raw)
for (i in 0 until arr.length()) {
val o = arr.getJSONObject(i)
val phones = mutableListOf<String>()
val pa = o.getJSONArray("phones")
for (j in 0 until pa.length()) phones.add(pa.getString(j))
groups.add(Group(o.getString("name"), phones))
}
} else {
// Migration depuis l'ancien format : liste unique de numéros
val legacy = sp(ctx).getString("phones", "") ?: ""
val phones = legacy.split(";").map { it.trim() }.filter { it.isNotEmpty() }
if (phones.isNotEmpty()) {
groups.add(Group(ctx.getString(R.string.default_group_name), phones))
saveGroups(ctx, groups)
}
}
return groups
}
fun saveGroups(ctx: Context, groups: List<Group>) {
val arr = JSONArray()
groups.forEach { g ->
arr.put(JSONObject().put("name", g.name).put("phones", JSONArray(g.phones)))
}
sp(ctx).edit().putString("groups", arr.toString()).apply()
}
fun getActiveGroupName(ctx: Context): String = sp(ctx).getString("activeGroup", "") ?: ""
fun setActiveGroupName(ctx: Context, name: String) {
sp(ctx).edit().putString("activeGroup", name).apply()
}
/** Groupe actif, ou le premier groupe si l'actif n'existe plus. */
fun getActiveGroup(ctx: Context): Group? {
val groups = getGroups(ctx)
if (groups.isEmpty()) return null
return groups.find { it.name == getActiveGroupName(ctx) } ?: groups.first()
}
}

View file

@ -1,57 +1,138 @@
package com.example.maposition package com.example.maposition
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.Toast import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
private val phoneRegex = Regex("""^\+?[0-9][0-9 .\-]{5,16}$""") private val phoneRegex = Regex("""^\+?[0-9][0-9 .\-]{5,16}$""")
private lateinit var groupsList: LinearLayout
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings) setContentView(R.layout.activity_settings)
val prefs = getSharedPreferences("MaPositionPrefs", Context.MODE_PRIVATE) groupsList = findViewById(R.id.groupsList)
val nameInput = findViewById<TextInputEditText>(R.id.nameInput) val nameInput = findViewById<TextInputEditText>(R.id.nameInput)
val phonesLayout = findViewById<TextInputLayout>(R.id.phonesLayout) nameInput.setText(Prefs.getName(this))
val phonesInput = findViewById<TextInputEditText>(R.id.phonesInput) nameInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, a: Int, b: Int, c: Int) {}
nameInput.setText(prefs.getString("name", "")) override fun onTextChanged(s: CharSequence?, a: Int, b: Int, c: Int) {}
phonesInput.setText(prefs.getString("phones", "")) override fun afterTextChanged(s: Editable?) {
Prefs.setName(this@SettingsActivity, s?.toString() ?: "")
}
})
findViewById<ImageButton>(R.id.backButton).setOnClickListener { finish() } findViewById<ImageButton>(R.id.backButton).setOnClickListener { finish() }
findViewById<MaterialButton>(R.id.addGroupButton).setOnClickListener { showGroupDialog(null) }
findViewById<MaterialButton>(R.id.saveButton).setOnClickListener { renderGroups()
val rawPhones = phonesInput.text.toString() }
val phones = rawPhones.split(";").map { it.trim() }.filter { it.isNotEmpty() }
when { private fun renderGroups() {
phones.isEmpty() -> { groupsList.removeAllViews()
phonesLayout.error = getString(R.string.error_no_phone) val groups = Prefs.getGroups(this)
return@setOnClickListener
} if (groups.isEmpty()) {
phones.any { !phoneRegex.matches(it) } -> { val empty = TextView(this).apply {
val bad = phones.first { !phoneRegex.matches(it) } text = getString(R.string.no_groups)
phonesLayout.error = getString(R.string.error_bad_phone, bad) setPadding(8, 24, 8, 24)
return@setOnClickListener
}
} }
phonesLayout.error = null groupsList.addView(empty)
return
}
prefs.edit() val activeName = Prefs.getActiveGroup(this)?.name
.putString("name", nameInput.text.toString().trim()) groups.forEach { g ->
.putString("phones", phones.joinToString(";")) val item = layoutInflater.inflate(R.layout.item_group, groupsList, false)
.apply() val title = if (g.name == activeName) {
getString(R.string.group_active_suffix, g.name)
Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show() } else {
finish() g.name
}
item.findViewById<TextView>(R.id.groupName).text = title
item.findViewById<TextView>(R.id.groupPhones).text = g.phones.joinToString(", ")
item.setOnClickListener { showGroupDialog(g) }
groupsList.addView(item)
} }
} }
private fun showGroupDialog(existing: Group?) {
val view = layoutInflater.inflate(R.layout.dialog_group, null)
val nameLayout = view.findViewById<TextInputLayout>(R.id.groupNameLayout)
val nameInput = view.findViewById<TextInputEditText>(R.id.groupNameInput)
val phonesLayout = view.findViewById<TextInputLayout>(R.id.groupPhonesLayout)
val phonesInput = view.findViewById<TextInputEditText>(R.id.groupPhonesInput)
if (existing != null) {
nameInput.setText(existing.name)
phonesInput.setText(existing.phones.joinToString(";"))
}
val builder = MaterialAlertDialogBuilder(this)
.setTitle(if (existing == null) R.string.group_new else R.string.group_edit)
.setView(view)
.setPositiveButton(R.string.save, null)
.setNegativeButton(android.R.string.cancel, null)
if (existing != null) builder.setNeutralButton(R.string.delete, null)
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val name = nameInput.text.toString().trim()
val phones = phonesInput.text.toString()
.split(";").map { it.trim() }.filter { it.isNotEmpty() }
nameLayout.error = null
phonesLayout.error = null
val groups = Prefs.getGroups(this)
when {
name.isEmpty() ->
nameLayout.error = getString(R.string.error_no_group_name)
groups.any { it.name == name && it.name != existing?.name } ->
nameLayout.error = getString(R.string.error_dup_group)
phones.isEmpty() ->
phonesLayout.error = getString(R.string.error_no_phone)
phones.any { !phoneRegex.matches(it) } ->
phonesLayout.error = getString(
R.string.error_bad_phone,
phones.first { !phoneRegex.matches(it) }
)
else -> {
val idx = groups.indexOfFirst { it.name == existing?.name }
if (idx >= 0) groups[idx] = Group(name, phones)
else groups.add(Group(name, phones))
Prefs.saveGroups(this, groups)
if (existing != null && Prefs.getActiveGroupName(this) == existing.name) {
Prefs.setActiveGroupName(this, name)
}
renderGroups()
dialog.dismiss()
}
}
}
if (existing != null) {
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
val groups = Prefs.getGroups(this)
groups.removeAll { it.name == existing.name }
Prefs.saveGroups(this, groups)
renderGroups()
dialog.dismiss()
}
}
}
dialog.show()
}
} }

View file

@ -23,10 +23,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="24dp" android:layout_marginStart="24dp"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:text="@string/app_name" android:text="@string/app_name"
android:textSize="28sp" android:textSize="28sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/settingsButton" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -38,23 +39,10 @@
android:text="@string/subtitle" android:text="@string/subtitle"
android:textColor="?attr/colorOnSurfaceVariant" android:textColor="?attr/colorOnSurfaceVariant"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@id/settingsButton" app:layout_constraintEnd_toEndOf="@id/titleText"
app:layout_constraintStart_toStartOf="@id/titleText" app:layout_constraintStart_toStartOf="@id/titleText"
app:layout_constraintTop_toBottomOf="@id/titleText" /> app:layout_constraintTop_toBottomOf="@id/titleText" />
<ImageButton
android:id="@+id/settingsButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/settings_title"
android:src="@drawable/ic_settings"
app:layout_constraintBottom_toBottomOf="@id/subtitleText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/titleText"
app:tint="?attr/colorOnSurfaceVariant" />
<FrameLayout <FrameLayout
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="256dp" android:layout_width="256dp"
@ -139,13 +127,37 @@
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp"> android:padding="16dp">
<TextView <LinearLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/recipients_label" android:gravity="center_vertical"
android:textColor="?attr/colorOnSurfaceVariant" android:orientation="horizontal">
android:textSize="13sp"
android:textStyle="bold" /> <TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/recipients_label"
android:textColor="?attr/colorOnSurfaceVariant"
android:textSize="13sp"
android:textStyle="bold" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_settings"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/groupChips"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:chipSpacingVertical="0dp"
app:singleLine="false"
app:singleSelection="true" />
<TextView <TextView
android:id="@+id/recipientsText" android:id="@+id/recipientsText"

View file

@ -12,78 +12,89 @@
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/bg_watermark" /> android:src="@drawable/bg_watermark" />
<LinearLayout <ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageButton <LinearLayout
android:id="@+id/backButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/back"
android:src="@drawable/ic_back"
app:tint="?attr/colorOnSurface" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/settings_title"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/nameLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/name_hint"
app:helperText="@string/name_helper">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/nameInput"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textPersonName" android:orientation="vertical"
android:maxLength="40" /> android:padding="24dp">
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <LinearLayout
android:id="@+id/phonesLayout" android:layout_width="match_parent"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_height="wrap_content"
android:layout_width="match_parent" android:gravity="center_vertical"
android:layout_height="wrap_content" android:orientation="horizontal">
android:layout_marginTop="24dp"
android:hint="@string/phones_hint"
app:helperText="@string/phones_helper">
<com.google.android.material.textfield.TextInputEditText <ImageButton
android:id="@+id/phonesInput" android:id="@+id/backButton"
android:layout_width="match_parent" android:layout_width="48dp"
android:layout_height="wrap_content" android:layout_height="48dp"
android:inputType="text" /> android:background="?attr/selectableItemBackgroundBorderless"
</com.google.android.material.textfield.TextInputLayout> android:contentDescription="@string/back"
android:src="@drawable/ic_back"
app:tint="?attr/colorOnSurface" />
<com.google.android.material.button.MaterialButton <TextView
android:id="@+id/saveButton" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="56dp" android:layout_marginStart="8dp"
android:layout_marginTop="32dp" android:text="@string/settings_title"
android:text="@string/save" android:textSize="24sp"
android:textSize="16sp" android:textStyle="bold" />
app:backgroundTint="@color/brand_blue" </LinearLayout>
app:cornerRadius="16dp" />
</LinearLayout> <com.google.android.material.textfield.TextInputLayout
android:id="@+id/nameLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hint="@string/name_hint"
app:helperText="@string/name_helper">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/nameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:maxLength="40" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="@string/groups_title"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/groups_help"
android:textColor="?attr/colorOnSurfaceVariant"
android:textSize="13sp" />
<LinearLayout
android:id="@+id/groupsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical" />
<com.google.android.material.button.MaterialButton
android:id="@+id/addGroupButton"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="16dp"
android:text="@string/add_group"
app:backgroundTint="@color/brand_blue"
app:cornerRadius="14dp" />
</LinearLayout>
</ScrollView>
</FrameLayout> </FrameLayout>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingTop="16dp"
android:paddingEnd="24dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/groupNameLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/group_name_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/groupNameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLength="24" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/groupPhonesLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/phones_hint"
app:helperText="@string/phones_helper">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/groupPhonesInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="14dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="14dp">
<TextView
android:id="@+id/groupName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/groupPhones"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="?attr/colorOnSurfaceVariant"
android:textSize="14sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -6,14 +6,28 @@
<!-- Écran principal --> <!-- Écran principal -->
<string name="send_button">ENVOYER\nMA POSITION</string> <string name="send_button">ENVOYER\nMA POSITION</string>
<string name="recipients_label">Destinataires — toucher pour modifier</string> <string name="recipients_label">Groupe de destinataires — toucher pour gérer</string>
<string name="recipients_none">Aucun destinataire configuré.\nTouchez ici pour en ajouter.</string> <string name="recipients_none">Aucun groupe de destinataires.\nTouchez ici pour en créer un.</string>
<!-- Groupes -->
<string name="groups_title">Groupes de destinataires</string>
<string name="groups_help">Créez des groupes (famille, amis, collègues…) et choisissez le groupe actif sur l\'écran principal.</string>
<string name="default_group_name">Mes contacts</string>
<string name="group_active_suffix">%1$s ✓ (groupe actif)</string>
<string name="group_new">Nouveau groupe</string>
<string name="group_edit">Modifier le groupe</string>
<string name="group_name_hint">Nom du groupe</string>
<string name="add_group">+ Ajouter un groupe</string>
<string name="no_groups">Aucun groupe pour l\'instant.</string>
<string name="delete">Supprimer</string>
<string name="error_no_group_name">Donnez un nom au groupe</string>
<string name="error_dup_group">Un groupe porte déjà ce nom</string>
<!-- Statuts --> <!-- Statuts -->
<string name="status_locating">Recherche de votre position…</string> <string name="status_locating">Recherche de votre position…</string>
<string name="status_fallback">GPS lent — utilisation de la dernière position connue…</string> <string name="status_fallback">GPS lent — utilisation de la dernière position connue…</string>
<string name="status_no_location">❌ Position introuvable. Vérifiez que la localisation est activée, puis réessayez.</string> <string name="status_no_location">❌ Position introuvable. Vérifiez que la localisation est activée, puis réessayez.</string>
<string name="status_no_recipients">Ajoutez d\'abord un destinataire.</string> <string name="status_no_recipients">Créez d\'abord un groupe de destinataires.</string>
<string name="status_need_permissions">❌ Les autorisations SMS et localisation sont nécessaires pour envoyer votre position.</string> <string name="status_need_permissions">❌ Les autorisations SMS et localisation sont nécessaires pour envoyer votre position.</string>
<string name="status_sending_progress">Envoi des SMS… (%1$d/%2$d)</string> <string name="status_sending_progress">Envoi des SMS… (%1$d/%2$d)</string>
<string name="status_sent_ok">✅ Position envoyée à %1$d destinataire(s)</string> <string name="status_sent_ok">✅ Position envoyée à %1$d destinataire(s)</string>