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.ScanHistoryEntity
|
||||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
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.ShoppingListItemEntity
|
||||||
|
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||||
import com.safebite.app.data.local.database.entity.UserProfileEntity
|
import com.safebite.app.data.local.database.entity.UserProfileEntity
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
@ -19,9 +20,10 @@ import com.safebite.app.data.local.database.entity.UserProfileEntity
|
|||||||
ProductCacheEntity::class,
|
ProductCacheEntity::class,
|
||||||
ScanHistoryEntity::class,
|
ScanHistoryEntity::class,
|
||||||
ShoppingListEntity::class,
|
ShoppingListEntity::class,
|
||||||
ShoppingListItemEntity::class
|
ShoppingListItemEntity::class,
|
||||||
|
ShoppingListMemberEntity::class
|
||||||
],
|
],
|
||||||
version = 5,
|
version = 6,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import androidx.room.Transaction
|
|||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
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.ShoppingListItemEntity
|
||||||
|
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,9 +20,12 @@ interface ShoppingListDao {
|
|||||||
|
|
||||||
// ── Shopping Lists ──────────────────────────────────────────────────────
|
// ── 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>>
|
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")
|
@Query("SELECT * FROM shopping_lists ORDER BY updatedAt DESC")
|
||||||
fun observeAllLists(): Flow<List<ShoppingListEntity>>
|
fun observeAllLists(): Flow<List<ShoppingListEntity>>
|
||||||
|
|
||||||
@ -69,6 +73,23 @@ interface ShoppingListDao {
|
|||||||
@Query("DELETE FROM shopping_list_items WHERE listId = :listId")
|
@Query("DELETE FROM shopping_list_items WHERE listId = :listId")
|
||||||
suspend fun deleteAllItems(listId: Long)
|
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 ───────────────────────────────────────────────────────────────
|
// ── Stats ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM shopping_list_items WHERE listId = :listId")
|
@Query("SELECT COUNT(*) FROM shopping_list_items WHERE listId = :listId")
|
||||||
|
|||||||
@ -71,7 +71,12 @@ data class ShoppingListEntity(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val createdAt: Long = System.currentTimeMillis(),
|
val createdAt: Long = System.currentTimeMillis(),
|
||||||
val updatedAt: 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(
|
@Entity(
|
||||||
@ -102,3 +107,25 @@ data class ShoppingListItemEntity(
|
|||||||
val tag: String? = null, // Tag visuel : "urgent", "offre", "whenever"
|
val tag: String? = null, // Tag visuel : "urgent", "offre", "whenever"
|
||||||
val addedAt: Long = System.currentTimeMillis()
|
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.dao.ShoppingListDao
|
||||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
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.ShoppingListItemEntity
|
||||||
|
import com.safebite.app.data.local.database.entity.ShoppingListMemberEntity
|
||||||
import com.safebite.app.domain.repository.ShoppingListRepository
|
import com.safebite.app.domain.repository.ShoppingListRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -22,11 +23,12 @@ class ShoppingListRepositoryImpl @Inject constructor(
|
|||||||
override suspend fun getListById(id: Long): ShoppingListEntity? =
|
override suspend fun getListById(id: Long): ShoppingListEntity? =
|
||||||
dao.getListById(id)
|
dao.getListById(id)
|
||||||
|
|
||||||
override suspend fun createList(name: String): Long {
|
override suspend fun createList(name: String, backgroundResName: String?): Long {
|
||||||
val list = ShoppingListEntity(
|
val list = ShoppingListEntity(
|
||||||
name = name,
|
name = name,
|
||||||
createdAt = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
updatedAt = System.currentTimeMillis()
|
updatedAt = System.currentTimeMillis(),
|
||||||
|
backgroundResName = backgroundResName
|
||||||
)
|
)
|
||||||
return dao.insertList(list)
|
return dao.insertList(list)
|
||||||
}
|
}
|
||||||
@ -81,4 +83,22 @@ class ShoppingListRepositoryImpl @Inject constructor(
|
|||||||
override suspend fun addItemToList(listId: Long, item: ShoppingListItemEntity) {
|
override suspend fun addItemToList(listId: Long, item: ShoppingListItemEntity) {
|
||||||
dao.addItemToList(listId, item)
|
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.ShoppingListEntity
|
||||||
import com.safebite.app.data.local.database.entity.ShoppingListItemEntity
|
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.AppLanguage
|
||||||
import com.safebite.app.domain.model.DetectionLanguage
|
import com.safebite.app.domain.model.DetectionLanguage
|
||||||
import com.safebite.app.domain.model.HealthStrictness
|
import com.safebite.app.domain.model.HealthStrictness
|
||||||
@ -78,7 +79,7 @@ interface ShoppingListRepository {
|
|||||||
fun observeActiveLists(): Flow<List<ShoppingListEntity>>
|
fun observeActiveLists(): Flow<List<ShoppingListEntity>>
|
||||||
fun observeAllLists(): Flow<List<ShoppingListEntity>>
|
fun observeAllLists(): Flow<List<ShoppingListEntity>>
|
||||||
suspend fun getListById(id: Long): 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 updateList(list: ShoppingListEntity)
|
||||||
suspend fun deleteList(list: ShoppingListEntity)
|
suspend fun deleteList(list: ShoppingListEntity)
|
||||||
suspend fun archiveList(id: Long)
|
suspend fun archiveList(id: Long)
|
||||||
@ -97,6 +98,13 @@ interface ShoppingListRepository {
|
|||||||
fun observeItemCount(listId: Long): Flow<Int>
|
fun observeItemCount(listId: Long): Flow<Int>
|
||||||
fun observeCheckedCount(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
|
// Helpers
|
||||||
suspend fun addItemToList(listId: Long, item: ShoppingListItemEntity)
|
suspend fun addItemToList(listId: Long, item: ShoppingListItemEntity)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,11 +98,14 @@ class GetShoppingListsUseCase @Inject constructor(
|
|||||||
fun observeActive() = repo.observeActiveLists()
|
fun observeActive() = repo.observeActiveLists()
|
||||||
fun observeAll() = repo.observeAllLists()
|
fun observeAll() = repo.observeAllLists()
|
||||||
suspend fun getList(id: Long) = repo.getListById(id)
|
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 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)
|
suspend fun deleteList(list: com.safebite.app.data.local.database.entity.ShoppingListEntity) = repo.deleteList(list)
|
||||||
fun observeItemCount(listId: Long) = repo.observeItemCount(listId)
|
fun observeItemCount(listId: Long) = repo.observeItemCount(listId)
|
||||||
fun observeCheckedCount(listId: Long) = repo.observeCheckedCount(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(
|
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.product.ProductDetailScreen
|
||||||
import com.safebite.app.presentation.screen.tracking.TrackingScreen
|
import com.safebite.app.presentation.screen.tracking.TrackingScreen
|
||||||
import com.safebite.app.presentation.screen.lists.ListDetailScreen
|
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.main.MainScreen
|
||||||
import com.safebite.app.presentation.screen.ocr.OcrCaptureScreen
|
import com.safebite.app.presentation.screen.ocr.OcrCaptureScreen
|
||||||
import com.safebite.app.presentation.screen.ocr.OcrReviewScreen
|
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) },
|
onOpenSettings = { navController.navigate(Screen.Settings.route) },
|
||||||
onOpenProfile = { id -> navController.navigate(Screen.ProfileEdit.edit(id)) },
|
onOpenProfile = { id -> navController.navigate(Screen.ProfileEdit.edit(id)) },
|
||||||
onOpenListDetail = { id, name -> navController.navigate(Screen.ListDetail.build(id, name)) },
|
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)) }
|
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 new() = "list/edit?id=0"
|
||||||
fun edit(id: Long) = "list/edit?id=$id"
|
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
|
package com.safebite.app.presentation.screen.lists
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import androidx.compose.animation.core.Animatable
|
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.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.MergeType
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.ShoppingCart
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
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.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.safebite.app.R
|
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.EmptyState
|
||||||
import com.safebite.app.presentation.common.components.PrimaryButton
|
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 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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ListsScreen(
|
fun ListsScreen(
|
||||||
onOpenList: (Long, String) -> Unit,
|
onOpenList: (Long, String) -> Unit,
|
||||||
onOpenScanner: () -> Unit,
|
onOpenScanner: () -> Unit,
|
||||||
|
onOpenListCreate: () -> Unit,
|
||||||
|
onOpenListSettings: (Long) -> Unit,
|
||||||
viewModel: ListsViewModel = hiltViewModel()
|
viewModel: ListsViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
var showCreateDialog by remember { mutableStateOf(false) }
|
val isEditMode by viewModel.isEditMode.collectAsStateWithLifecycle()
|
||||||
|
val dimens = LocalDimens.current
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
@ -81,17 +94,32 @@ fun ListsScreen(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.lists_title)) },
|
title = { Text(stringResource(R.string.lists_title)) },
|
||||||
actions = {
|
actions = {
|
||||||
val addContentDesc = stringResource(R.string.a11y_add)
|
when (val s = state) {
|
||||||
IconButton(
|
is ListsViewModel.UiState.Success -> {
|
||||||
onClick = { showCreateDialog = true },
|
if (isEditMode) {
|
||||||
modifier = Modifier.semantics {
|
TextButton(onClick = { viewModel.toggleEditMode() }) {
|
||||||
contentDescription = addContentDesc
|
Text(stringResource(R.string.lists_done))
|
||||||
}
|
}
|
||||||
) {
|
} else if (s.lists.isNotEmpty()) {
|
||||||
Icon(Icons.Filled.Add, contentDescription = null)
|
TextButton(onClick = { viewModel.toggleEditMode() }) {
|
||||||
|
Text(stringResource(R.string.lists_edit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 ->
|
) { padding ->
|
||||||
Box(
|
Box(
|
||||||
@ -101,39 +129,30 @@ fun ListsScreen(
|
|||||||
) {
|
) {
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
is ListsViewModel.UiState.Loading -> {
|
is ListsViewModel.UiState.Loading -> {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is ListsViewModel.UiState.Empty -> {
|
is ListsViewModel.UiState.Empty -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "Aucune liste",
|
title = stringResource(R.string.lists_empty),
|
||||||
message = s.message,
|
message = s.message,
|
||||||
emoji = "📋",
|
emoji = "📋",
|
||||||
action = {
|
action = {
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
text = "Créer une liste",
|
text = stringResource(R.string.lists_new),
|
||||||
onClick = { showCreateDialog = true }
|
onClick = onOpenListCreate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ListsViewModel.UiState.Success -> {
|
is ListsViewModel.UiState.Success -> {
|
||||||
LazyColumn(
|
ReorderableList(
|
||||||
modifier = Modifier.fillMaxSize(),
|
items = s.lists,
|
||||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
isEditMode = isEditMode,
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
onItemClick = { item -> onOpenList(item.list.id, item.list.name) },
|
||||||
) {
|
onSettingsClick = { item -> onOpenListSettings(item.list.id) },
|
||||||
items(s.lists, key = { it.list.id }) { item ->
|
onReorder = { from, to -> viewModel.reorderLists(from, to) }
|
||||||
ShoppingListCard(
|
|
||||||
item = item,
|
|
||||||
onClick = { onOpenList(item.list.id, item.list.name) },
|
|
||||||
onDelete = { viewModel.deleteList(item.list) },
|
|
||||||
onMerge = { /* TODO: Ouvrir dialog fusion */ }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
is ListsViewModel.UiState.Error -> {
|
is ListsViewModel.UiState.Error -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
title = "Erreur",
|
title = "Erreur",
|
||||||
@ -144,149 +163,216 @@ fun ListsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showCreateDialog) {
|
@Composable
|
||||||
CreateListDialog(
|
private fun ReorderableList(
|
||||||
onDismiss = { showCreateDialog = false },
|
items: List<ListsViewModel.ShoppingListWithStats>,
|
||||||
onCreate = { name ->
|
isEditMode: Boolean,
|
||||||
viewModel.createList(name)
|
onItemClick: (ListsViewModel.ShoppingListWithStats) -> Unit,
|
||||||
showCreateDialog = false
|
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
|
@Composable
|
||||||
private fun ShoppingListCard(
|
private fun ShoppingListCard(
|
||||||
item: ListsViewModel.ShoppingListWithStats,
|
item: ListsViewModel.ShoppingListWithStats,
|
||||||
|
isEditMode: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onSettingsClick: () -> Unit
|
||||||
onMerge: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val dimens = LocalDimens.current
|
val dimens = LocalDimens.current
|
||||||
val progress = if (item.itemCount > 0) item.checkedCount.toFloat() / item.itemCount else 0f
|
val bg = backgroundByResName(item.list.backgroundResName)
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onClick),
|
.height(160.dp)
|
||||||
colors = CardDefaults.cardColors(
|
.then(if (!isEditMode) Modifier.clickable(onClick = onClick) else Modifier),
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
shape = RoundedCornerShape(16.dp),
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||||
) {
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// Background image
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(dimens.spacingMd)
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = onSettingsClick,
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
) {
|
||||||
|
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(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
// Item count badge
|
||||||
imageVector = Icons.Filled.ShoppingCart,
|
val remaining = item.itemCount - item.checkedCount
|
||||||
contentDescription = null,
|
val badgeColor = if (remaining > 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.tertiary
|
||||||
modifier = Modifier.size(32.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
Spacer(Modifier.padding(start = dimens.spacingSm))
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
Text(
|
||||||
text = item.list.name,
|
text = "$remaining articles",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
color = Color.White,
|
||||||
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)
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showMenu,
|
|
||||||
onDismissRequest = { showMenu = false }
|
|
||||||
) {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(dimens.spacingSm))
|
|
||||||
|
|
||||||
// Barre de progression
|
|
||||||
LinearProgressIndicator(
|
|
||||||
progress = { progress },
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.background(badgeColor.copy(alpha = 0.85f), RoundedCornerShape(12.dp))
|
||||||
.height(6.dp)
|
.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||||
.clip(MaterialTheme.shapes.small),
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
// Member avatars
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy((-8).dp)
|
||||||
|
) {
|
||||||
|
item.members.take(3).forEach { member ->
|
||||||
|
MemberAvatar(member = member)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CreateListDialog(
|
private fun MemberAvatar(member: ShoppingListMemberEntity) {
|
||||||
onDismiss: () -> Unit,
|
Box(
|
||||||
onCreate: (String) -> Unit
|
modifier = Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
var listName by remember { mutableStateOf("") }
|
Text(
|
||||||
|
text = member.name.take(1).uppercase(),
|
||||||
AlertDialog(
|
style = MaterialTheme.typography.labelMedium,
|
||||||
onDismissRequest = onDismiss,
|
fontWeight = FontWeight.Bold,
|
||||||
title = { Text("Nouvelle liste") },
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3,10 +3,12 @@ package com.safebite.app.presentation.screen.lists
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
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 com.safebite.app.domain.usecase.GetShoppingListsUseCase
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
@ -35,25 +37,30 @@ class ListsViewModel @Inject constructor(
|
|||||||
data class ShoppingListWithStats(
|
data class ShoppingListWithStats(
|
||||||
val list: ShoppingListEntity,
|
val list: ShoppingListEntity,
|
||||||
val itemCount: Int,
|
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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val state: StateFlow<UiState> = getShoppingListsUseCase.observeActive()
|
val state: StateFlow<UiState> = getShoppingListsUseCase.observeActive()
|
||||||
.flatMapLatest { lists ->
|
.flatMapLatest { lists ->
|
||||||
if (lists.isEmpty()) {
|
if (lists.isEmpty()) {
|
||||||
flowOf(UiState.Empty("Aucune liste de courses. Créez votre première liste !"))
|
flowOf(UiState.Empty("Aucune liste de courses. Créez votre première liste !"))
|
||||||
} else {
|
} else {
|
||||||
val statsFlows = lists.map { list ->
|
val statsFlows = lists.sortedBy { it.displayOrder }.map { list ->
|
||||||
combine(
|
combine(
|
||||||
getShoppingListsUseCase.observeItemCount(list.id),
|
getShoppingListsUseCase.observeItemCount(list.id),
|
||||||
getShoppingListsUseCase.observeCheckedCount(list.id)
|
getShoppingListsUseCase.observeCheckedCount(list.id),
|
||||||
) { itemCount, checkedCount ->
|
getShoppingListsUseCase.observeMembers(list.id)
|
||||||
ShoppingListWithStats(list, itemCount, checkedCount)
|
) { itemCount, checkedCount, members ->
|
||||||
|
ShoppingListWithStats(list, itemCount, checkedCount, members.take(3))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
combine(statsFlows) { array ->
|
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
|
initialValue = UiState.Loading
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createList(name: String) {
|
fun createList(name: String, backgroundResName: String? = null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
getShoppingListsUseCase.createList(name)
|
getShoppingListsUseCase.createList(name, backgroundResName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,4 +81,22 @@ class ListsViewModel @Inject constructor(
|
|||||||
getShoppingListsUseCase.deleteList(list)
|
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,
|
onOpenProfile: (Long) -> Unit,
|
||||||
onOpenListDetail: (Long, String) -> Unit,
|
onOpenListDetail: (Long, String) -> Unit,
|
||||||
onOpenHistoryItem: (String) -> Unit,
|
onOpenHistoryItem: (String) -> Unit,
|
||||||
|
onOpenListCreate: () -> Unit = {},
|
||||||
|
onOpenListSettings: (Long) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
@ -141,7 +143,9 @@ fun MainScreen(
|
|||||||
composable(Screen.Lists.route) {
|
composable(Screen.Lists.route) {
|
||||||
ListsScreen(
|
ListsScreen(
|
||||||
onOpenList = { id, name -> onOpenListDetail(id, name) },
|
onOpenList = { id, name -> onOpenListDetail(id, name) },
|
||||||
onOpenScanner = onOpenScanner
|
onOpenScanner = onOpenScanner,
|
||||||
|
onOpenListCreate = onOpenListCreate,
|
||||||
|
onOpenListSettings = onOpenListSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Screen.Tracking.route) {
|
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_name">Name</string>
|
||||||
<string name="list_item_description">Description</string>
|
<string name="list_item_description">Description</string>
|
||||||
<string name="list_add_item_confirm">Add</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>
|
</resources>
|
||||||
|
|||||||
@ -75,6 +75,32 @@
|
|||||||
<string name="lists_empty">Aucune liste</string>
|
<string name="lists_empty">Aucune liste</string>
|
||||||
<string name="lists_products_count">%1$d produits</string>
|
<string name="lists_products_count">%1$d produits</string>
|
||||||
<string name="lists_bought_count">%1$d achetés</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 -->
|
<!-- Tracking -->
|
||||||
<string name="tracking_title">Suivi</string>
|
<string name="tracking_title">Suivi</string>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
MAJOR=1
|
MAJOR=1
|
||||||
MINOR=14
|
MINOR=15
|
||||||
PATCH=0
|
PATCH=0
|
||||||
CODE=18
|
CODE=19
|
||||||
|
|||||||