feat: add shopping list member management, display order, and customization fields with drag-to-reorder UI
- Add `ShoppingListMemberEntity` table with foreign key cascade to shopping lists - Add `backgroundResName`, `region`, `sortType`, `displayOrder`, `visibleCategories` fields to `ShoppingListEntity` - Increment database version to 6 - Implement member CRUD operations in DAO and repository layers - Add `observeMembers`, `addMember`, `updateMember`, `removeMember` methods across data/domain layers - Update
@ -11,6 +11,7 @@ import com.safebite.app.data.local.database.entity.ProductCacheEntity
|
||||
import com.safebite.app.data.local.database.entity.ScanHistoryEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListItemEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||
import com.safebite.app.data.local.database.entity.UserProfileEntity
|
||||
|
||||
@Database(
|
||||
@ -19,9 +20,10 @@ import com.safebite.app.data.local.database.entity.UserProfileEntity
|
||||
ProductCacheEntity::class,
|
||||
ScanHistoryEntity::class,
|
||||
ShoppingListEntity::class,
|
||||
ShoppingListItemEntity::class
|
||||
ShoppingListItemEntity::class,
|
||||
ShoppingListMemberEntity::class
|
||||
],
|
||||
version = 5,
|
||||
version = 6,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
||||
@ -9,6 +9,7 @@ import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListItemEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
@ -19,9 +20,12 @@ interface ShoppingListDao {
|
||||
|
||||
// ── Shopping Lists ──────────────────────────────────────────────────────
|
||||
|
||||
@Query("SELECT * FROM shopping_lists WHERE isArchived = 0 ORDER BY updatedAt DESC")
|
||||
@Query("SELECT * FROM shopping_lists WHERE isArchived = 0 ORDER BY displayOrder ASC, updatedAt DESC")
|
||||
fun observeActiveLists(): Flow<List<ShoppingListEntity>>
|
||||
|
||||
@Query("SELECT * FROM shopping_lists WHERE isArchived = 0 ORDER BY displayOrder ASC, updatedAt DESC")
|
||||
fun observeActiveListsOrdered(): Flow<List<ShoppingListEntity>>
|
||||
|
||||
@Query("SELECT * FROM shopping_lists ORDER BY updatedAt DESC")
|
||||
fun observeAllLists(): Flow<List<ShoppingListEntity>>
|
||||
|
||||
@ -69,6 +73,23 @@ interface ShoppingListDao {
|
||||
@Query("DELETE FROM shopping_list_items WHERE listId = :listId")
|
||||
suspend fun deleteAllItems(listId: Long)
|
||||
|
||||
// ── Members ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Query("SELECT * FROM shopping_list_members WHERE listId = :listId ORDER BY joinedAt ASC")
|
||||
fun observeMembers(listId: Long): Flow<List<ShoppingListMemberEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertMember(member: ShoppingListMemberEntity): Long
|
||||
|
||||
@Update
|
||||
suspend fun updateMember(member: ShoppingListMemberEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteMember(member: ShoppingListMemberEntity)
|
||||
|
||||
@Query("DELETE FROM shopping_list_members WHERE listId = :listId")
|
||||
suspend fun deleteAllMembers(listId: Long)
|
||||
|
||||
// ── Stats ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Query("SELECT COUNT(*) FROM shopping_list_items WHERE listId = :listId")
|
||||
|
||||
@ -71,7 +71,12 @@ data class ShoppingListEntity(
|
||||
val name: String,
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
val updatedAt: Long = System.currentTimeMillis(),
|
||||
val isArchived: Boolean = false
|
||||
val isArchived: Boolean = false,
|
||||
val backgroundResName: String? = null,
|
||||
val region: String? = null,
|
||||
val sortType: String = "category",
|
||||
val displayOrder: Int = 0,
|
||||
val visibleCategories: String? = null
|
||||
)
|
||||
|
||||
@Entity(
|
||||
@ -102,3 +107,25 @@ data class ShoppingListItemEntity(
|
||||
val tag: String? = null, // Tag visuel : "urgent", "offre", "whenever"
|
||||
val addedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "shopping_list_members",
|
||||
foreignKeys = [
|
||||
androidx.room.ForeignKey(
|
||||
entity = ShoppingListEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["listId"],
|
||||
onDelete = androidx.room.ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [androidx.room.Index("listId")]
|
||||
)
|
||||
data class ShoppingListMemberEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0L,
|
||||
val listId: Long,
|
||||
val name: String,
|
||||
val email: String,
|
||||
val avatarUrl: String? = null,
|
||||
val role: String = "member", // "owner" | "member"
|
||||
val joinedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@ -3,6 +3,7 @@ package com.safebite.app.data.repository
|
||||
import com.safebite.app.data.local.database.dao.ShoppingListDao
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListItemEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||
import com.safebite.app.domain.repository.ShoppingListRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
@ -22,11 +23,12 @@ class ShoppingListRepositoryImpl @Inject constructor(
|
||||
override suspend fun getListById(id: Long): ShoppingListEntity? =
|
||||
dao.getListById(id)
|
||||
|
||||
override suspend fun createList(name: String): Long {
|
||||
override suspend fun createList(name: String, backgroundResName: String?): Long {
|
||||
val list = ShoppingListEntity(
|
||||
name = name,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
updatedAt = System.currentTimeMillis()
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
backgroundResName = backgroundResName
|
||||
)
|
||||
return dao.insertList(list)
|
||||
}
|
||||
@ -81,4 +83,22 @@ class ShoppingListRepositoryImpl @Inject constructor(
|
||||
override suspend fun addItemToList(listId: Long, item: ShoppingListItemEntity) {
|
||||
dao.addItemToList(listId, item)
|
||||
}
|
||||
|
||||
override fun observeMembers(listId: Long): Flow<List<ShoppingListMemberEntity>> =
|
||||
dao.observeMembers(listId)
|
||||
|
||||
override suspend fun addMember(member: ShoppingListMemberEntity): Long =
|
||||
dao.insertMember(member)
|
||||
|
||||
override suspend fun updateMember(member: ShoppingListMemberEntity) {
|
||||
dao.updateMember(member)
|
||||
}
|
||||
|
||||
override suspend fun removeMember(member: ShoppingListMemberEntity) {
|
||||
dao.deleteMember(member)
|
||||
}
|
||||
|
||||
override suspend fun deleteAllMembers(listId: Long) {
|
||||
dao.deleteAllMembers(listId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.safebite.app.domain.repository
|
||||
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListItemEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||
import com.safebite.app.domain.model.AppLanguage
|
||||
import com.safebite.app.domain.model.DetectionLanguage
|
||||
import com.safebite.app.domain.model.HealthStrictness
|
||||
@ -78,7 +79,7 @@ interface ShoppingListRepository {
|
||||
fun observeActiveLists(): Flow<List<ShoppingListEntity>>
|
||||
fun observeAllLists(): Flow<List<ShoppingListEntity>>
|
||||
suspend fun getListById(id: Long): ShoppingListEntity?
|
||||
suspend fun createList(name: String): Long
|
||||
suspend fun createList(name: String, backgroundResName: String? = null): Long
|
||||
suspend fun updateList(list: ShoppingListEntity)
|
||||
suspend fun deleteList(list: ShoppingListEntity)
|
||||
suspend fun archiveList(id: Long)
|
||||
@ -97,6 +98,13 @@ interface ShoppingListRepository {
|
||||
fun observeItemCount(listId: Long): Flow<Int>
|
||||
fun observeCheckedCount(listId: Long): Flow<Int>
|
||||
|
||||
// Members
|
||||
fun observeMembers(listId: Long): Flow<List<ShoppingListMemberEntity>>
|
||||
suspend fun addMember(member: ShoppingListMemberEntity): Long
|
||||
suspend fun updateMember(member: ShoppingListMemberEntity)
|
||||
suspend fun removeMember(member: ShoppingListMemberEntity)
|
||||
suspend fun deleteAllMembers(listId: Long)
|
||||
|
||||
// Helpers
|
||||
suspend fun addItemToList(listId: Long, item: ShoppingListItemEntity)
|
||||
}
|
||||
|
||||
@ -98,11 +98,14 @@ class GetShoppingListsUseCase @Inject constructor(
|
||||
fun observeActive() = repo.observeActiveLists()
|
||||
fun observeAll() = repo.observeAllLists()
|
||||
suspend fun getList(id: Long) = repo.getListById(id)
|
||||
suspend fun createList(name: String) = repo.createList(name)
|
||||
suspend fun createList(name: String, backgroundResName: String? = null) = repo.createList(name, backgroundResName)
|
||||
suspend fun updateList(list: com.safebite.app.data.local.database.entity.ShoppingListEntity) = repo.updateList(list)
|
||||
suspend fun deleteList(list: com.safebite.app.data.local.database.entity.ShoppingListEntity) = repo.deleteList(list)
|
||||
fun observeItemCount(listId: Long) = repo.observeItemCount(listId)
|
||||
fun observeCheckedCount(listId: Long) = repo.observeCheckedCount(listId)
|
||||
fun observeMembers(listId: Long) = repo.observeMembers(listId)
|
||||
suspend fun addMember(member: com.safebite.app.data.local.database.entity.ShoppingListMemberEntity) = repo.addMember(member)
|
||||
suspend fun removeMember(member: com.safebite.app.data.local.database.entity.ShoppingListMemberEntity) = repo.removeMember(member)
|
||||
}
|
||||
|
||||
class ManageShoppingListUseCase @Inject constructor(
|
||||
|
||||
@ -15,6 +15,13 @@ import androidx.navigation.navArgument
|
||||
import com.safebite.app.presentation.screen.product.ProductDetailScreen
|
||||
import com.safebite.app.presentation.screen.tracking.TrackingScreen
|
||||
import com.safebite.app.presentation.screen.lists.ListDetailScreen
|
||||
import com.safebite.app.presentation.screen.lists.ListsScreen
|
||||
import com.safebite.app.presentation.screen.lists.create.CreateListScreen
|
||||
import com.safebite.app.presentation.screen.lists.settings.ListSettingsScreen
|
||||
import com.safebite.app.presentation.screen.lists.settings.ListSortScreen
|
||||
import com.safebite.app.presentation.screen.lists.settings.ListRegionScreen
|
||||
import com.safebite.app.presentation.screen.lists.settings.ListNameImageScreen
|
||||
import com.safebite.app.presentation.screen.lists.settings.ListMembersScreen
|
||||
import com.safebite.app.presentation.screen.main.MainScreen
|
||||
import com.safebite.app.presentation.screen.ocr.OcrCaptureScreen
|
||||
import com.safebite.app.presentation.screen.ocr.OcrReviewScreen
|
||||
@ -85,7 +92,9 @@ fun SafeBiteNavGraph(onboardingCompleted: Boolean, showSplash: Boolean = false)
|
||||
onOpenSettings = { navController.navigate(Screen.Settings.route) },
|
||||
onOpenProfile = { id -> navController.navigate(Screen.ProfileEdit.edit(id)) },
|
||||
onOpenListDetail = { id, name -> navController.navigate(Screen.ListDetail.build(id, name)) },
|
||||
onOpenHistoryItem = { barcode -> navController.navigate(Screen.Result.fromBarcode(barcode)) }
|
||||
onOpenHistoryItem = { barcode -> navController.navigate(Screen.Result.fromBarcode(barcode)) },
|
||||
onOpenListCreate = { navController.navigate(Screen.ListCreate.route) },
|
||||
onOpenListSettings = { id -> navController.navigate(Screen.ListSettings.build(id)) }
|
||||
)
|
||||
}
|
||||
|
||||
@ -217,5 +226,67 @@ fun SafeBiteNavGraph(onboardingCompleted: Boolean, showSplash: Boolean = false)
|
||||
onOpenProduct = { b -> navController.navigate(Screen.ProductDetail.build(b)) }
|
||||
)
|
||||
}
|
||||
|
||||
// ── List management (refonte) ──
|
||||
composable(Screen.ListCreate.route) {
|
||||
CreateListScreen(
|
||||
onBack = { navController.popBackStack() },
|
||||
onListCreated = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Screen.ListSettings.route,
|
||||
arguments = listOf(navArgument("id") { type = NavType.LongType })
|
||||
) { entry ->
|
||||
val listId = entry.arguments?.getLong("id") ?: 0L
|
||||
ListSettingsScreen(
|
||||
listId = listId,
|
||||
onBack = { navController.popBackStack() },
|
||||
onOpenSort = { navController.navigate(Screen.ListSort.build(listId)) },
|
||||
onOpenRegion = { navController.navigate(Screen.ListRegion.build(listId)) },
|
||||
onOpenNameImage = { navController.navigate(Screen.ListNameImage.build(listId)) },
|
||||
onOpenMembers = { navController.navigate(Screen.ListMembers.build(listId)) }
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Screen.ListSort.route,
|
||||
arguments = listOf(navArgument("id") { type = NavType.LongType })
|
||||
) { entry ->
|
||||
val listId = entry.arguments?.getLong("id") ?: 0L
|
||||
ListSortScreen(
|
||||
listId = listId,
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Screen.ListRegion.route,
|
||||
arguments = listOf(navArgument("id") { type = NavType.LongType })
|
||||
) { entry ->
|
||||
val listId = entry.arguments?.getLong("id") ?: 0L
|
||||
ListRegionScreen(
|
||||
listId = listId,
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Screen.ListNameImage.route,
|
||||
arguments = listOf(navArgument("id") { type = NavType.LongType })
|
||||
) { entry ->
|
||||
val listId = entry.arguments?.getLong("id") ?: 0L
|
||||
ListNameImageScreen(
|
||||
listId = listId,
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Screen.ListMembers.route,
|
||||
arguments = listOf(navArgument("id") { type = NavType.LongType })
|
||||
) { entry ->
|
||||
val listId = entry.arguments?.getLong("id") ?: 0L
|
||||
ListMembersScreen(
|
||||
listId = listId,
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,24 @@ sealed class Screen(val route: String) {
|
||||
fun new() = "list/edit?id=0"
|
||||
fun edit(id: Long) = "list/edit?id=$id"
|
||||
}
|
||||
|
||||
// ── List management (refonte) ──
|
||||
data object ListCreate : Screen("list/create")
|
||||
data object ListSettings : Screen("list/settings/{id}") {
|
||||
fun build(id: Long) = "list/settings/$id"
|
||||
}
|
||||
data object ListSort : Screen("list/sort/{id}") {
|
||||
fun build(id: Long) = "list/sort/$id"
|
||||
}
|
||||
data object ListRegion : Screen("list/region/{id}") {
|
||||
fun build(id: Long) = "list/region/$id"
|
||||
}
|
||||
data object ListNameImage : Screen("list/nameimage/{id}") {
|
||||
fun build(id: Long) = "list/nameimage/$id"
|
||||
}
|
||||
data object ListMembers : Screen("list/members/{id}") {
|
||||
fun build(id: Long) = "list/members/$id"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,79 +1,92 @@
|
||||
package com.safebite.app.presentation.screen.lists
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.MergeType
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.ShoppingCart
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.safebite.app.R
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||
import com.safebite.app.presentation.common.components.EmptyState
|
||||
import com.safebite.app.presentation.common.components.PrimaryButton
|
||||
import com.safebite.app.presentation.screen.lists.util.backgroundByResName
|
||||
import com.safebite.app.presentation.theme.LocalDimens
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Écran Listes (spec UX §5.5 - Flow 5).
|
||||
*
|
||||
* Affiche la liste des courses avec progression et alertes allergies.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListsScreen(
|
||||
onOpenList: (Long, String) -> Unit,
|
||||
onOpenScanner: () -> Unit,
|
||||
onOpenListCreate: () -> Unit,
|
||||
onOpenListSettings: (Long) -> Unit,
|
||||
viewModel: ListsViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
var showCreateDialog by remember { mutableStateOf(false) }
|
||||
val isEditMode by viewModel.isEditMode.collectAsStateWithLifecycle()
|
||||
val dimens = LocalDimens.current
|
||||
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
@ -81,17 +94,32 @@ fun ListsScreen(
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.lists_title)) },
|
||||
actions = {
|
||||
val addContentDesc = stringResource(R.string.a11y_add)
|
||||
IconButton(
|
||||
onClick = { showCreateDialog = true },
|
||||
modifier = Modifier.semantics {
|
||||
contentDescription = addContentDesc
|
||||
when (val s = state) {
|
||||
is ListsViewModel.UiState.Success -> {
|
||||
if (isEditMode) {
|
||||
TextButton(onClick = { viewModel.toggleEditMode() }) {
|
||||
Text(stringResource(R.string.lists_done))
|
||||
}
|
||||
} else if (s.lists.isNotEmpty()) {
|
||||
TextButton(onClick = { viewModel.toggleEditMode() }) {
|
||||
Text(stringResource(R.string.lists_edit))
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Filled.Add, contentDescription = null)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = onOpenListCreate,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Icon(Icons.Filled.Add, contentDescription = stringResource(R.string.lists_new))
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
@ -101,38 +129,29 @@ fun ListsScreen(
|
||||
) {
|
||||
when (val s = state) {
|
||||
is ListsViewModel.UiState.Loading -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
is ListsViewModel.UiState.Empty -> {
|
||||
EmptyState(
|
||||
title = "Aucune liste",
|
||||
title = stringResource(R.string.lists_empty),
|
||||
message = s.message,
|
||||
emoji = "📋",
|
||||
action = {
|
||||
PrimaryButton(
|
||||
text = "Créer une liste",
|
||||
onClick = { showCreateDialog = true }
|
||||
text = stringResource(R.string.lists_new),
|
||||
onClick = onOpenListCreate
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
is ListsViewModel.UiState.Success -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(s.lists, key = { it.list.id }) { item ->
|
||||
ShoppingListCard(
|
||||
item = item,
|
||||
onClick = { onOpenList(item.list.id, item.list.name) },
|
||||
onDelete = { viewModel.deleteList(item.list) },
|
||||
onMerge = { /* TODO: Ouvrir dialog fusion */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
ReorderableList(
|
||||
items = s.lists,
|
||||
isEditMode = isEditMode,
|
||||
onItemClick = { item -> onOpenList(item.list.id, item.list.name) },
|
||||
onSettingsClick = { item -> onOpenListSettings(item.list.id) },
|
||||
onReorder = { from, to -> viewModel.reorderLists(from, to) }
|
||||
)
|
||||
}
|
||||
is ListsViewModel.UiState.Error -> {
|
||||
EmptyState(
|
||||
@ -144,149 +163,216 @@ fun ListsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showCreateDialog) {
|
||||
CreateListDialog(
|
||||
onDismiss = { showCreateDialog = false },
|
||||
onCreate = { name ->
|
||||
viewModel.createList(name)
|
||||
showCreateDialog = false
|
||||
@Composable
|
||||
private fun ReorderableList(
|
||||
items: List<ListsViewModel.ShoppingListWithStats>,
|
||||
isEditMode: Boolean,
|
||||
onItemClick: (ListsViewModel.ShoppingListWithStats) -> Unit,
|
||||
onSettingsClick: (ListsViewModel.ShoppingListWithStats) -> Unit,
|
||||
onReorder: (Int, Int) -> Unit
|
||||
) {
|
||||
var draggedIndex by remember { mutableStateOf<Int?>(null) }
|
||||
var dragOffsetY by remember { mutableFloatStateOf(0f) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val itemHeight = 160.dp
|
||||
val itemPx = with(LocalContext.current.resources.displayMetrics) { itemHeight.value * density }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = items,
|
||||
key = { _, item -> item.list.id }
|
||||
) { index, item ->
|
||||
val isDragged = draggedIndex == index
|
||||
val zIndex = if (isDragged) 1f else 0f
|
||||
val elevation = if (isDragged) 8.dp else 2.dp
|
||||
val offsetY = if (isDragged) dragOffsetY.roundToInt() else 0
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.zIndex(zIndex)
|
||||
.offset { IntOffset(0, offsetY) }
|
||||
.graphicsLayer {
|
||||
scaleX = if (isDragged) 1.02f else 1f
|
||||
scaleY = if (isDragged) 1.02f else 1f
|
||||
}
|
||||
.then(
|
||||
if (isEditMode) {
|
||||
Modifier.pointerInput(Unit) {
|
||||
detectDragGesturesAfterLongPress(
|
||||
onDragStart = { draggedIndex = index },
|
||||
onDragEnd = {
|
||||
draggedIndex?.let { from ->
|
||||
val to = (from + (dragOffsetY / itemPx).roundToInt())
|
||||
.coerceIn(0, items.size - 1)
|
||||
if (from != to) onReorder(from, to)
|
||||
}
|
||||
draggedIndex = null
|
||||
dragOffsetY = 0f
|
||||
},
|
||||
onDragCancel = {
|
||||
draggedIndex = null
|
||||
dragOffsetY = 0f
|
||||
},
|
||||
onDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
dragOffsetY += dragAmount.y
|
||||
}
|
||||
)
|
||||
}
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
ShoppingListCard(
|
||||
item = item,
|
||||
isEditMode = isEditMode,
|
||||
onClick = { onItemClick(item) },
|
||||
onSettingsClick = { onSettingsClick(item) }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShoppingListCard(
|
||||
item: ListsViewModel.ShoppingListWithStats,
|
||||
isEditMode: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onMerge: () -> Unit
|
||||
onSettingsClick: () -> Unit
|
||||
) {
|
||||
val dimens = LocalDimens.current
|
||||
val progress = if (item.itemCount > 0) item.checkedCount.toFloat() / item.itemCount else 0f
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
val bg = backgroundByResName(item.list.backgroundResName)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
.height(160.dp)
|
||||
.then(if (!isEditMode) Modifier.clickable(onClick = onClick) else Modifier),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(dimens.spacingMd)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ShoppingCart,
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Background image
|
||||
if (bg != null) {
|
||||
Image(
|
||||
painter = painterResource(id = bg.drawableRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(Modifier.padding(start = dimens.spacingSm))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.list.name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "${item.itemCount} produits • ${item.checkedCount} achetés",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
// Menu actions
|
||||
Box {
|
||||
val moreOptionsDesc = stringResource(R.string.a11y_more_options)
|
||||
IconButton(
|
||||
onClick = { showMenu = true },
|
||||
modifier = Modifier.semantics {
|
||||
contentDescription = moreOptionsDesc
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Filled.MoreVert, contentDescription = null)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(dimens.spacingMd)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
if (isEditMode) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.DragHandle,
|
||||
contentDescription = "Reorder",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
|
||||
IconButton(
|
||||
onClick = onSettingsClick,
|
||||
modifier = Modifier.size(32.dp)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Fusionner avec...") },
|
||||
leadingIcon = { Icon(Icons.Filled.MergeType, contentDescription = null) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onMerge()
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Supprimer") },
|
||||
leadingIcon = { Icon(Icons.Filled.Delete, contentDescription = null) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
onDelete()
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings,
|
||||
contentDescription = stringResource(R.string.lists_settings),
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// List name
|
||||
Text(
|
||||
text = item.list.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Item count badge
|
||||
val remaining = item.itemCount - item.checkedCount
|
||||
val badgeColor = if (remaining > 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.tertiary
|
||||
Text(
|
||||
text = "$remaining articles",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = Color.White,
|
||||
modifier = Modifier
|
||||
.background(badgeColor.copy(alpha = 0.85f), RoundedCornerShape(12.dp))
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
// Member avatars
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy((-8).dp)
|
||||
) {
|
||||
item.members.take(3).forEach { member ->
|
||||
MemberAvatar(member = member)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(dimens.spacingSm))
|
||||
|
||||
// Barre de progression
|
||||
LinearProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(6.dp)
|
||||
.clip(MaterialTheme.shapes.small),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateListDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onCreate: (String) -> Unit
|
||||
) {
|
||||
var listName by remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Nouvelle liste") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = listName,
|
||||
onValueChange = { listName = it },
|
||||
label = { Text("Nom de la liste") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { if (listName.isNotBlank()) onCreate(listName) },
|
||||
enabled = listName.isNotBlank()
|
||||
) {
|
||||
Text("Créer")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Annuler")
|
||||
}
|
||||
}
|
||||
)
|
||||
private fun MemberAvatar(member: ShoppingListMemberEntity) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = member.name.take(1).uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,12 @@ package com.safebite.app.presentation.screen.lists
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||
import com.safebite.app.domain.usecase.GetShoppingListsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
@ -35,25 +37,30 @@ class ListsViewModel @Inject constructor(
|
||||
data class ShoppingListWithStats(
|
||||
val list: ShoppingListEntity,
|
||||
val itemCount: Int,
|
||||
val checkedCount: Int
|
||||
val checkedCount: Int,
|
||||
val members: List<ShoppingListMemberEntity> = emptyList()
|
||||
)
|
||||
|
||||
private val _isEditMode = MutableStateFlow(false)
|
||||
val isEditMode: StateFlow<Boolean> = _isEditMode
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val state: StateFlow<UiState> = getShoppingListsUseCase.observeActive()
|
||||
.flatMapLatest { lists ->
|
||||
if (lists.isEmpty()) {
|
||||
flowOf(UiState.Empty("Aucune liste de courses. Créez votre première liste !"))
|
||||
} else {
|
||||
val statsFlows = lists.map { list ->
|
||||
val statsFlows = lists.sortedBy { it.displayOrder }.map { list ->
|
||||
combine(
|
||||
getShoppingListsUseCase.observeItemCount(list.id),
|
||||
getShoppingListsUseCase.observeCheckedCount(list.id)
|
||||
) { itemCount, checkedCount ->
|
||||
ShoppingListWithStats(list, itemCount, checkedCount)
|
||||
getShoppingListsUseCase.observeCheckedCount(list.id),
|
||||
getShoppingListsUseCase.observeMembers(list.id)
|
||||
) { itemCount, checkedCount, members ->
|
||||
ShoppingListWithStats(list, itemCount, checkedCount, members.take(3))
|
||||
}
|
||||
}
|
||||
combine(statsFlows) { array ->
|
||||
UiState.Success(array.toList())
|
||||
UiState.Success(array.toList().sortedBy { it.list.displayOrder })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,9 +70,9 @@ class ListsViewModel @Inject constructor(
|
||||
initialValue = UiState.Loading
|
||||
)
|
||||
|
||||
fun createList(name: String) {
|
||||
fun createList(name: String, backgroundResName: String? = null) {
|
||||
viewModelScope.launch {
|
||||
getShoppingListsUseCase.createList(name)
|
||||
getShoppingListsUseCase.createList(name, backgroundResName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,4 +81,22 @@ class ListsViewModel @Inject constructor(
|
||||
getShoppingListsUseCase.deleteList(list)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleEditMode() {
|
||||
_isEditMode.value = !_isEditMode.value
|
||||
}
|
||||
|
||||
fun reorderLists(fromIndex: Int, toIndex: Int) {
|
||||
viewModelScope.launch {
|
||||
val current = state.value as? UiState.Success ?: return@launch
|
||||
val mutable = current.lists.toMutableList()
|
||||
val moved = mutable.removeAt(fromIndex)
|
||||
mutable.add(toIndex.coerceIn(0, mutable.size), moved)
|
||||
mutable.forEachIndexed { index, item ->
|
||||
getShoppingListsUseCase.updateList(
|
||||
item.list.copy(displayOrder = index)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,166 @@
|
||||
package com.safebite.app.presentation.screen.lists.create
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.safebite.app.R
|
||||
import com.safebite.app.presentation.screen.lists.ListsViewModel
|
||||
import com.safebite.app.presentation.screen.lists.util.allListBackgrounds
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CreateListScreen(
|
||||
onBack: () -> Unit,
|
||||
onListCreated: () -> Unit,
|
||||
viewModel: ListsViewModel = hiltViewModel()
|
||||
) {
|
||||
var listName by remember { mutableStateOf("") }
|
||||
var selectedBg by remember { mutableStateOf(allListBackgrounds.firstOrNull()?.resName) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.list_create_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (listName.isNotBlank()) {
|
||||
viewModel.createList(listName, selectedBg)
|
||||
onListCreated()
|
||||
}
|
||||
},
|
||||
enabled = listName.isNotBlank()
|
||||
) {
|
||||
Text(stringResource(R.string.list_create_next))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = listName,
|
||||
onValueChange = { listName = it },
|
||||
label = { Text(stringResource(R.string.list_name_hint)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.list_choose_background),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(allListBackgrounds) { bg ->
|
||||
val isSelected = selectedBg == bg.resName
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.5f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = if (isSelected) BorderStroke(3.dp, MaterialTheme.colorScheme.primary) else null,
|
||||
onClick = { selectedBg = bg.resName }
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Image(
|
||||
painter = painterResource(id = bg.drawableRes),
|
||||
contentDescription = bg.label,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.background(Color.White, RoundedCornerShape(14.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,189 @@
|
||||
package com.safebite.app.presentation.screen.lists.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.RemoveCircleOutline
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.safebite.app.R
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||
import com.safebite.app.presentation.screen.lists.ListsViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListMembersScreen(
|
||||
listId: Long,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ListsViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val listData = (state as? ListsViewModel.UiState.Success)?.lists?.firstOrNull { it.list.id == listId }
|
||||
val members = listData?.members ?: emptyList()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.list_members_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
if (listData == null) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
} else {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Text(
|
||||
text = stringResource(R.string.list_members_count, members.size),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(members) { member ->
|
||||
MemberRow(
|
||||
member = member,
|
||||
onRemove = {
|
||||
// TODO: remove member via viewmodel/usecase
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Button(
|
||||
onClick = { /* TODO: invite UI placeholder */ },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.list_invite_member))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberRow(
|
||||
member: ShoppingListMemberEntity,
|
||||
onRemove: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = member.name.take(1).uppercase(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = member.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = member.email,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onRemove) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.RemoveCircleOutline,
|
||||
contentDescription = stringResource(R.string.list_remove_member),
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
package com.safebite.app.presentation.screen.lists.settings
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.safebite.app.R
|
||||
import com.safebite.app.presentation.screen.lists.ListsViewModel
|
||||
import com.safebite.app.presentation.screen.lists.util.allListBackgrounds
|
||||
import com.safebite.app.presentation.screen.lists.util.backgroundByResName
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListNameImageScreen(
|
||||
listId: Long,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ListsViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val listData = (state as? ListsViewModel.UiState.Success)?.lists?.firstOrNull { it.list.id == listId }
|
||||
var listName by remember(listData?.list?.name) { mutableStateOf(listData?.list?.name ?: "") }
|
||||
var selectedBg by remember(listData?.list?.backgroundResName) {
|
||||
mutableStateOf(listData?.list?.backgroundResName)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.list_name_image_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
if (listData == null) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
} else {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Preview
|
||||
val bg = backgroundByResName(selectedBg)
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(120.dp),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (bg != null) {
|
||||
Image(
|
||||
painter = painterResource(id = bg.drawableRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = listName.ifBlank { listData.list.name },
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = listName,
|
||||
onValueChange = { listName = it },
|
||||
label = { Text(stringResource(R.string.list_name_hint)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.list_choose_background),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(allListBackgrounds) { bg ->
|
||||
val isSelected = selectedBg == bg.resName
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.5f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
border = if (isSelected) BorderStroke(3.dp, MaterialTheme.colorScheme.primary) else null,
|
||||
onClick = { selectedBg = bg.resName }
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Image(
|
||||
painter = painterResource(id = bg.drawableRes),
|
||||
contentDescription = bg.label,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.background(Color.White, RoundedCornerShape(14.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
val updated = listData.list.copy(
|
||||
name = listName.ifBlank { listData.list.name },
|
||||
backgroundResName = selectedBg
|
||||
)
|
||||
// TODO: update via viewmodel/usecase
|
||||
onBack()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.action_save))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
package com.safebite.app.presentation.screen.lists.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.safebite.app.R
|
||||
import com.safebite.app.presentation.screen.lists.ListsViewModel
|
||||
|
||||
private val availableRegions = listOf(
|
||||
"Allemagne" to "de",
|
||||
"Australie" to "au",
|
||||
"Autriche" to "at",
|
||||
"Canada" to "ca",
|
||||
"Espagne" to "es",
|
||||
"France" to "fr",
|
||||
"Hongrie" to "hu",
|
||||
"Italie" to "it",
|
||||
"Norvège" to "no",
|
||||
"Pays-Bas" to "nl",
|
||||
"Pologne" to "pl",
|
||||
"Portugal" to "pt",
|
||||
"Royaume-Uni" to "gb",
|
||||
"Russie" to "ru",
|
||||
"Suisse (Allemand)" to "ch_de",
|
||||
"Suisse (français)" to "ch_fr"
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListRegionScreen(
|
||||
listId: Long,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ListsViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val listData = (state as? ListsViewModel.UiState.Success)?.lists?.firstOrNull { it.list.id == listId }
|
||||
var selectedRegion by remember(listData?.list?.region) {
|
||||
mutableStateOf(listData?.list?.region)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.list_region_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.list_region_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(availableRegions) { (name, code) ->
|
||||
val isSelected = selectedRegion == code
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
selectedRegion = code
|
||||
listData?.let {
|
||||
// TODO: persist via viewmodel/usecase
|
||||
}
|
||||
}
|
||||
.padding(vertical = 14.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primary),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
package com.safebite.app.presentation.screen.lists.settings
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Brush
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.safebite.app.R
|
||||
import com.safebite.app.presentation.screen.lists.ListsViewModel
|
||||
import com.safebite.app.presentation.screen.lists.util.backgroundByResName
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListSettingsScreen(
|
||||
listId: Long,
|
||||
onBack: () -> Unit,
|
||||
onOpenSort: () -> Unit,
|
||||
onOpenRegion: () -> Unit,
|
||||
onOpenNameImage: () -> Unit,
|
||||
onOpenMembers: () -> Unit,
|
||||
viewModel: ListsViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val listData = (state as? ListsViewModel.UiState.Success)?.lists?.firstOrNull { it.list.id == listId }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.list_settings_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
if (listData == null) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
} else {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Header card with list preview
|
||||
val bg = backgroundByResName(listData.list.backgroundResName)
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(140.dp),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (bg != null) {
|
||||
Image(
|
||||
painter = painterResource(id = bg.drawableRes),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.35f))
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = listData.list.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Settings grid
|
||||
Text(
|
||||
text = stringResource(R.string.list_personalize),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
item {
|
||||
SettingsTile(
|
||||
icon = Icons.AutoMirrored.Filled.Sort,
|
||||
label = stringResource(R.string.list_sort),
|
||||
onClick = onOpenSort
|
||||
)
|
||||
}
|
||||
item {
|
||||
SettingsTile(
|
||||
icon = Icons.Filled.Language,
|
||||
label = stringResource(R.string.list_region_language),
|
||||
onClick = onOpenRegion
|
||||
)
|
||||
}
|
||||
item {
|
||||
SettingsTile(
|
||||
icon = Icons.Filled.People,
|
||||
label = stringResource(R.string.list_members),
|
||||
onClick = onOpenMembers
|
||||
)
|
||||
}
|
||||
item {
|
||||
SettingsTile(
|
||||
icon = Icons.Filled.Brush,
|
||||
label = stringResource(R.string.list_name_image),
|
||||
onClick = onOpenNameImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Leave list button
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.deleteList(listData.list)
|
||||
onBack()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.list_leave))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsTile(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1.2f),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(32.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
package com.safebite.app.presentation.screen.lists.settings
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.DragHandle
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.safebite.app.R
|
||||
import com.safebite.app.domain.engine.CatalogProvider
|
||||
import com.safebite.app.presentation.screen.lists.ListsViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ListSortScreen(
|
||||
listId: Long,
|
||||
onBack: () -> Unit,
|
||||
viewModel: ListsViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val listData = (state as? ListsViewModel.UiState.Success)?.lists?.firstOrNull { it.list.id == listId }
|
||||
val catalog = remember { CatalogProvider() }
|
||||
var visibleCategories by remember(listData?.list?.visibleCategories) {
|
||||
mutableStateOf(
|
||||
listData?.list?.visibleCategories?.split(",")?.toSet() ?: catalog.categories.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.list_sort_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Text(
|
||||
text = stringResource(R.string.action_save),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.clickable {
|
||||
listData?.let {
|
||||
val updated = it.list.copy(
|
||||
visibleCategories = visibleCategories.joinToString(",")
|
||||
)
|
||||
// TODO: update via viewmodel/usecase
|
||||
}
|
||||
onBack()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.list_sort_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.list_sort_preview),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
itemsIndexed(catalog.categories) { _, category ->
|
||||
val isVisible = category in visibleCategories
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable {
|
||||
visibleCategories = if (isVisible) {
|
||||
visibleCategories - category
|
||||
} else {
|
||||
visibleCategories + category
|
||||
}
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = category,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = {
|
||||
visibleCategories = if (isVisible) {
|
||||
visibleCategories - category
|
||||
} else {
|
||||
visibleCategories + category
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = if (isVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
|
||||
contentDescription = null,
|
||||
tint = if (isVisible) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Filled.DragHandle,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.safebite.app.presentation.screen.lists.util
|
||||
|
||||
import com.safebite.app.R
|
||||
|
||||
data class ListBackground(
|
||||
val resName: String,
|
||||
val label: String,
|
||||
val drawableRes: Int
|
||||
)
|
||||
|
||||
val allListBackgrounds: List<ListBackground> = listOf(
|
||||
ListBackground("bg_animaux", "Animaux", R.drawable.bg_animaux),
|
||||
ListBackground("bg_baby", "Bébé", R.drawable.bg_baby),
|
||||
ListBackground("bg_epicerie", "Épicerie", R.drawable.bg_epicerie),
|
||||
ListBackground("bg_epicerie2", "Épicerie 2", R.drawable.bg_epicerie2),
|
||||
ListBackground("bg_jardinage", "Maison & Jardin", R.drawable.bg_jardinage),
|
||||
ListBackground("bg_office", "Bureau", R.drawable.bg_office),
|
||||
ListBackground("bg_party", "Fête", R.drawable.bg_party),
|
||||
ListBackground("bg_pharmacie", "Pharmacie", R.drawable.bg_pharmacie),
|
||||
ListBackground("bg_plage", "Plage", R.drawable.bg_plage),
|
||||
ListBackground("bg_renovation", "Rénovation", R.drawable.bg_renovation)
|
||||
)
|
||||
|
||||
fun backgroundByResName(name: String?): ListBackground? =
|
||||
allListBackgrounds.firstOrNull { it.resName == name }
|
||||
|
||||
fun backgroundLabel(name: String?): String =
|
||||
backgroundByResName(name)?.label ?: ""
|
||||
@ -70,6 +70,8 @@ fun MainScreen(
|
||||
onOpenProfile: (Long) -> Unit,
|
||||
onOpenListDetail: (Long, String) -> Unit,
|
||||
onOpenHistoryItem: (String) -> Unit,
|
||||
onOpenListCreate: () -> Unit = {},
|
||||
onOpenListSettings: (Long) -> Unit = {},
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
@ -141,7 +143,9 @@ fun MainScreen(
|
||||
composable(Screen.Lists.route) {
|
||||
ListsScreen(
|
||||
onOpenList = { id, name -> onOpenListDetail(id, name) },
|
||||
onOpenScanner = onOpenScanner
|
||||
onOpenScanner = onOpenScanner,
|
||||
onOpenListCreate = onOpenListCreate,
|
||||
onOpenListSettings = onOpenListSettings
|
||||
)
|
||||
}
|
||||
composable(Screen.Tracking.route) {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 912 KiB After Width: | Height: | Size: 912 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
@ -206,4 +206,37 @@
|
||||
<string name="list_item_name">Name</string>
|
||||
<string name="list_item_description">Description</string>
|
||||
<string name="list_add_item_confirm">Add</string>
|
||||
|
||||
<!-- Lists -->
|
||||
<string name="lists_title">My lists</string>
|
||||
<string name="lists_new">New list</string>
|
||||
<string name="lists_empty">No list</string>
|
||||
<string name="lists_products_count">%1$d products</string>
|
||||
<string name="lists_bought_count">%1$d bought</string>
|
||||
<string name="lists_edit">EDIT</string>
|
||||
<string name="lists_done">DONE</string>
|
||||
<string name="lists_settings">Settings</string>
|
||||
|
||||
<!-- List management (refonte) -->
|
||||
<string name="list_create_title">New list</string>
|
||||
<string name="list_create_next">Next</string>
|
||||
<string name="list_name_hint">List name</string>
|
||||
<string name="list_choose_background">Choose a background</string>
|
||||
<string name="list_settings_title">List settings</string>
|
||||
<string name="list_personalize">Personalize list</string>
|
||||
<string name="list_sort">Sort</string>
|
||||
<string name="list_region_language">Region & Language</string>
|
||||
<string name="list_members">List members</string>
|
||||
<string name="list_name_image">Name & Image</string>
|
||||
<string name="list_leave">Leave this list</string>
|
||||
<string name="list_sort_title">Sort</string>
|
||||
<string name="list_sort_description">Bring! sorts items to buy by category order. Arrange them like in your usual store!</string>
|
||||
<string name="list_sort_preview">List preview</string>
|
||||
<string name="list_region_title">Region & Language</string>
|
||||
<string name="list_region_description">In which region do you shop with this list? We will adapt the assortment for you.</string>
|
||||
<string name="list_members_title">List members</string>
|
||||
<string name="list_members_count">%1$d members</string>
|
||||
<string name="list_invite_member">Invite a new person</string>
|
||||
<string name="list_remove_member">Remove</string>
|
||||
<string name="list_name_image_title">Edit list</string>
|
||||
</resources>
|
||||
|
||||
@ -75,6 +75,32 @@
|
||||
<string name="lists_empty">Aucune liste</string>
|
||||
<string name="lists_products_count">%1$d produits</string>
|
||||
<string name="lists_bought_count">%1$d achetés</string>
|
||||
<string name="lists_edit">ÉDITER</string>
|
||||
<string name="lists_done">TERMINÉ</string>
|
||||
<string name="lists_settings">Paramètres</string>
|
||||
|
||||
<!-- List management (refonte) -->
|
||||
<string name="list_create_title">Nouvelle liste</string>
|
||||
<string name="list_create_next">Suivant</string>
|
||||
<string name="list_name_hint">Nom de la liste</string>
|
||||
<string name="list_choose_background">Choisir un fond</string>
|
||||
<string name="list_settings_title">Paramètres de la liste</string>
|
||||
<string name="list_personalize">Personnaliser la liste</string>
|
||||
<string name="list_sort">Trier</string>
|
||||
<string name="list_region_language">Région & Langue</string>
|
||||
<string name="list_members">Membres de la liste</string>
|
||||
<string name="list_name_image">Nom & Image</string>
|
||||
<string name="list_leave">Quitter cette liste</string>
|
||||
<string name="list_sort_title">Trier</string>
|
||||
<string name="list_sort_description">Bring! range les articles à acheter selon l\'ordre des catégories. Classez-les comme dans votre magasin habituel !</string>
|
||||
<string name="list_sort_preview">Aperçu de la liste</string>
|
||||
<string name="list_region_title">Région & Langue</string>
|
||||
<string name="list_region_description">Dans quelle région fais-tu tes achats avec cette liste ? Nous adapterons l\'assortiment pour toi.</string>
|
||||
<string name="list_members_title">Membres de la liste</string>
|
||||
<string name="list_members_count">%1$d membres</string>
|
||||
<string name="list_invite_member">Inviter une nouvelle personne</string>
|
||||
<string name="list_remove_member">Retirer</string>
|
||||
<string name="list_name_image_title">Éditer la liste</string>
|
||||
|
||||
<!-- Tracking -->
|
||||
<string name="tracking_title">Suivi</string>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
MAJOR=1
|
||||
MINOR=14
|
||||
MINOR=15
|
||||
PATCH=0
|
||||
CODE=18
|
||||
CODE=19
|
||||
|
||||