Add haptic feedback (thx Jérémy). better handling of removal animation.
This commit is contained in:
parent
b27149edaf
commit
2c5337f3dc
6 changed files with 58 additions and 15 deletions
|
|
@ -62,9 +62,10 @@ class InventoryFactory @Inject constructor() {
|
||||||
items: Map<String, Item>,
|
items: Map<String, Item>,
|
||||||
fires: List<FireItem>,
|
fires: List<FireItem>,
|
||||||
order: Map<String, Int>,
|
order: Map<String, Int>,
|
||||||
|
parent: Map<String, String?>,
|
||||||
): Map<String?, List<Any>> {
|
): Map<String?, List<Any>> {
|
||||||
return fires
|
return fires
|
||||||
.groupBy { it.parentId }
|
.groupBy { parent[it.id] }
|
||||||
.mapValues { (_, fires) ->
|
.mapValues { (_, fires) ->
|
||||||
fires
|
fires
|
||||||
.asSequence()
|
.asSequence()
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
@ -70,6 +71,7 @@ fun InventoryPage(
|
||||||
containerWidth: Dp = 64.dp,
|
containerWidth: Dp = 64.dp,
|
||||||
) {
|
) {
|
||||||
val overlay = LocalRollOverlay.current
|
val overlay = LocalRollOverlay.current
|
||||||
|
val feedback = LocalHapticFeedback.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val selectedContainer = inventoryViewModel.selectedContainer
|
val selectedContainer = inventoryViewModel.selectedContainer
|
||||||
|
|
@ -78,10 +80,16 @@ fun InventoryPage(
|
||||||
|
|
||||||
val dragDropState = rememberInventoryDragDropState(
|
val dragDropState = rememberInventoryDragDropState(
|
||||||
containerWidth = containerWidth,
|
containerWidth = containerWidth,
|
||||||
isItemDraggable = inventoryViewModel::isItemDraggable,
|
isItemDraggable = {
|
||||||
|
inventoryViewModel.isItemDraggable(it, feedback)
|
||||||
|
},
|
||||||
isItemsMovable = inventoryViewModel::isItemsMovable,
|
isItemsMovable = inventoryViewModel::isItemsMovable,
|
||||||
onItemMove = inventoryViewModel::onItemMove,
|
onItemMove = { from, to ->
|
||||||
onItemRelease = inventoryViewModel::onItemRelease,
|
inventoryViewModel.onItemMove(from, to, feedback)
|
||||||
|
},
|
||||||
|
onItemRelease = { from, to ->
|
||||||
|
inventoryViewModel.onItemRelease(from, to, feedback)
|
||||||
|
},
|
||||||
onItemLongClick = {
|
onItemLongClick = {
|
||||||
// on container long press we want to display the detail dialog.
|
// on container long press we want to display the detail dialog.
|
||||||
if (it.type == Type.CONTAINER) {
|
if (it.type == Type.CONTAINER) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -41,6 +43,7 @@ class InventoryViewModel @Inject constructor(
|
||||||
private val character = savedStateHandle.characterSheetArgument.name
|
private val character = savedStateHandle.characterSheetArgument.name
|
||||||
|
|
||||||
private val orderOverrider = MutableStateFlow<Map<String, Int>>(emptyMap())
|
private val orderOverrider = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||||
|
private val parentOverrider = MutableStateFlow<Map<String, String?>>(emptyMap())
|
||||||
private val _selectedContainerId = MutableStateFlow<String?>(null)
|
private val _selectedContainerId = MutableStateFlow<String?>(null)
|
||||||
|
|
||||||
private val _containers: StateFlow<List<InventoryContainerUio>> = combine(
|
private val _containers: StateFlow<List<InventoryContainerUio>> = combine(
|
||||||
|
|
@ -77,6 +80,7 @@ class InventoryViewModel @Inject constructor(
|
||||||
itemRepository.data,
|
itemRepository.data,
|
||||||
fireRepository.getInventory(character),
|
fireRepository.getInventory(character),
|
||||||
orderOverrider,
|
orderOverrider,
|
||||||
|
parentOverrider,
|
||||||
transform = inventoryFactory::fromModelsToItemUio,
|
transform = inventoryFactory::fromModelsToItemUio,
|
||||||
).stateIn(
|
).stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
|
|
@ -88,6 +92,24 @@ class InventoryViewModel @Inject constructor(
|
||||||
@Stable
|
@Stable
|
||||||
get() = _items.collectAsState()
|
get() = _items.collectAsState()
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
// Listen to fire repository to update the orderOverrider as soon as firebase is updated.
|
||||||
|
fireRepository.getInventory(character = character).collect { fires ->
|
||||||
|
orderOverrider.value = fires.associate { fire -> fire.id to fire.order }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
// Listen to fire repository to update the orderOverrider as soon as firebase is updated.
|
||||||
|
fireRepository.getInventory(character = character).collect { fires ->
|
||||||
|
parentOverrider.value = fires.associate { fire -> fire.id to fire.parentId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getItem(element: Element): Any? {
|
fun getItem(element: Element): Any? {
|
||||||
return when (element.type) {
|
return when (element.type) {
|
||||||
Type.CONTAINER -> _containers.value.getOrNull(element.index)
|
Type.CONTAINER -> _containers.value.getOrNull(element.index)
|
||||||
|
|
@ -99,11 +121,17 @@ class InventoryViewModel @Inject constructor(
|
||||||
_selectedContainerId.value = id
|
_selectedContainerId.value = id
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isItemDraggable(element: Element): Boolean {
|
fun isItemDraggable(element: Element, feedback: HapticFeedback): Boolean {
|
||||||
Log.d("InventoryViewModel", "isItemDraggable(element:${element}")
|
Log.d("InventoryViewModel", "isItemDraggable(element:${element}")
|
||||||
return when (val item = getItem(element = element)) {
|
return when (val item = getItem(element = element)) {
|
||||||
is InventoryItemUio -> true
|
is InventoryItemUio -> {
|
||||||
is InventoryContainerUio -> item.id != null
|
feedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
is InventoryContainerUio -> {
|
||||||
|
feedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
item.id != null
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +163,7 @@ class InventoryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onItemMove(from: Element, to: Element) {
|
fun onItemMove(from: Element, to: Element, feedback: HapticFeedback) {
|
||||||
if (from != to) {
|
if (from != to) {
|
||||||
Log.d("InventoryViewModel", "onItemMove(from:${from}, to:${to})")
|
Log.d("InventoryViewModel", "onItemMove(from:${from}, to:${to})")
|
||||||
val fromItem: Any? = getItem(element = from)
|
val fromItem: Any? = getItem(element = from)
|
||||||
|
|
@ -146,6 +174,8 @@ class InventoryViewModel @Inject constructor(
|
||||||
val refFromItem = itemRepository.find(id = fromItem.id)
|
val refFromItem = itemRepository.find(id = fromItem.id)
|
||||||
val refToItem = itemRepository.find(id = toItem.id)
|
val refToItem = itemRepository.find(id = toItem.id)
|
||||||
if (refFromItem?.type != null && refFromItem.type == refToItem?.type) {
|
if (refFromItem?.type != null && refFromItem.type == refToItem?.type) {
|
||||||
|
// perform feedback
|
||||||
|
feedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
// reorder locally
|
// reorder locally
|
||||||
orderOverrider.value = orderOverrider.value.toMutableMap().also {
|
orderOverrider.value = orderOverrider.value.toMutableMap().also {
|
||||||
it[fromItem.id] = toItem.order
|
it[fromItem.id] = toItem.order
|
||||||
|
|
@ -168,6 +198,8 @@ class InventoryViewModel @Inject constructor(
|
||||||
val refFromItem = itemRepository.find(id = fromItem.id)
|
val refFromItem = itemRepository.find(id = fromItem.id)
|
||||||
val refToItem = itemRepository.find(id = toItem.id)
|
val refToItem = itemRepository.find(id = toItem.id)
|
||||||
if (refFromItem?.type != null && refFromItem.type == refToItem?.type) {
|
if (refFromItem?.type != null && refFromItem.type == refToItem?.type) {
|
||||||
|
// perform feedback
|
||||||
|
feedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
// reorder locally
|
// reorder locally
|
||||||
orderOverrider.value = orderOverrider.value.toMutableMap().also {
|
orderOverrider.value = orderOverrider.value.toMutableMap().also {
|
||||||
it[fromItem.id] = toItem.order
|
it[fromItem.id] = toItem.order
|
||||||
|
|
@ -189,9 +221,7 @@ class InventoryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onItemRelease(from: Element, to: Element) {
|
fun onItemRelease(from: Element, to: Element, feedback: HapticFeedback) {
|
||||||
// clean the order override map, this is only needed when moving items around.
|
|
||||||
orderOverrider.value = emptyMap()
|
|
||||||
// only do something when an item is release on top of a container.
|
// only do something when an item is release on top of a container.
|
||||||
if (from.type == Type.ITEM && to.type == Type.CONTAINER) {
|
if (from.type == Type.ITEM && to.type == Type.CONTAINER) {
|
||||||
// fetch the itemId & containerId.
|
// fetch the itemId & containerId.
|
||||||
|
|
@ -215,7 +245,12 @@ class InventoryViewModel @Inject constructor(
|
||||||
val typedOrderContainerFires = fires.filter {
|
val typedOrderContainerFires = fires.filter {
|
||||||
typedItems[it.id] != null && it.parentId == containerId
|
typedItems[it.id] != null && it.parentId == containerId
|
||||||
}
|
}
|
||||||
|
parentOverrider.value = parentOverrider.value.toMutableMap().also { override ->
|
||||||
|
override[fire.id] = containerId
|
||||||
|
}
|
||||||
|
// perform feedback
|
||||||
|
feedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
|
// update the data layer.
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
// remove the item from the list
|
// remove the item from the list
|
||||||
fireRepository.removeItem(
|
fireRepository.removeItem(
|
||||||
|
|
@ -247,9 +282,6 @@ class InventoryViewModel @Inject constructor(
|
||||||
defaultParentId: String? = _selectedContainerId.value,
|
defaultParentId: String? = _selectedContainerId.value,
|
||||||
quantity: Int,
|
quantity: Int,
|
||||||
) {
|
) {
|
||||||
// clean the order override map, this is only needed when moving items around.
|
|
||||||
orderOverrider.value = emptyMap()
|
|
||||||
|
|
||||||
val item = itemRepository.find(id = itemId)
|
val item = itemRepository.find(id = itemId)
|
||||||
|
|
||||||
// check the new quantity to decide if we update or remove the item
|
// check the new quantity to decide if we update or remove the item
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,14 @@ object ImageCache {
|
||||||
"https://drive.google.com/uc?export=view&id=1Dc0hBGXTNP8dnYqHJlSsK1WgWAsWtCV9" to R.drawable.ic_drive_lantern_of_revealing,
|
"https://drive.google.com/uc?export=view&id=1Dc0hBGXTNP8dnYqHJlSsK1WgWAsWtCV9" to R.drawable.ic_drive_lantern_of_revealing,
|
||||||
"https://drive.google.com/uc?export=view&id=1FX7HpnxLUXFPqrKGvCYyJNkUSwFacKh9" to R.drawable.ic_silver_coin_pile_unfaded,
|
"https://drive.google.com/uc?export=view&id=1FX7HpnxLUXFPqrKGvCYyJNkUSwFacKh9" to R.drawable.ic_silver_coin_pile_unfaded,
|
||||||
"https://drive.google.com/uc?export=view&id=1jJkltHU5HzCuU5ixVly_J5Dr0stEawEV" to R.drawable.ic_drive_deck_of_card,
|
"https://drive.google.com/uc?export=view&id=1jJkltHU5HzCuU5ixVly_J5Dr0stEawEV" to R.drawable.ic_drive_deck_of_card,
|
||||||
|
"https://drive.google.com/uc?export=view&id=1JhUikzOesAyGCtCL9bFW_HRdqM8hj1sf" to R.drawable.ic_drive_bear_trap,
|
||||||
"https://drive.google.com/uc?export=view&id=1Jo0Nk_Mj4j-VKvBe0W6_73uE_tKUgPUF" to R.drawable.ic_drive_kitsune,
|
"https://drive.google.com/uc?export=view&id=1Jo0Nk_Mj4j-VKvBe0W6_73uE_tKUgPUF" to R.drawable.ic_drive_kitsune,
|
||||||
"https://drive.google.com/uc?export=view&id=1KPlgQK1C3lfZn3ZHTHd-dqiSINjQpnH9" to R.drawable.ic_drive_ring_of_kazan,
|
"https://drive.google.com/uc?export=view&id=1KPlgQK1C3lfZn3ZHTHd-dqiSINjQpnH9" to R.drawable.ic_drive_ring_of_kazan,
|
||||||
"https://drive.google.com/uc?export=view&id=1KVXSKHI8JgU1Wnhp9rmEZ1f4rsUfoV2a" to R.drawable.ic_gold_coin_pile_unfaded,
|
"https://drive.google.com/uc?export=view&id=1KVXSKHI8JgU1Wnhp9rmEZ1f4rsUfoV2a" to R.drawable.ic_gold_coin_pile_unfaded,
|
||||||
"https://drive.google.com/uc?export=view&id=1lLu7o1h1pkXuNRbHJ2dnxQLKyrNRxV3P" to R.drawable.ic_drive_bear_claw_neckless,
|
"https://drive.google.com/uc?export=view&id=1lLu7o1h1pkXuNRbHJ2dnxQLKyrNRxV3P" to R.drawable.ic_drive_bear_claw_neckless,
|
||||||
"https://drive.google.com/uc?export=view&id=1nazdbGFLyiGNMYvPhVXiZoeyBxDtYMtI" to R.drawable.ic_electrum_coin_pile_unfaded,
|
"https://drive.google.com/uc?export=view&id=1nazdbGFLyiGNMYvPhVXiZoeyBxDtYMtI" to R.drawable.ic_electrum_coin_pile_unfaded,
|
||||||
"https://drive.google.com/uc?export=view&id=1p5OSYPeeKa-niiTa9GFChdpnu0FqkswG" to R.drawable.ic_drive_harlequin,
|
"https://drive.google.com/uc?export=view&id=1p5OSYPeeKa-niiTa9GFChdpnu0FqkswG" to R.drawable.ic_drive_harlequin,
|
||||||
|
"https://drive.google.com/uc?export=view&id=1TDTCkVZb520gdndC4FmAneWDGgf9wmDM" to R.drawable.ic_drive_chalk_stick,
|
||||||
"https://drive.google.com/uc?export=view&id=1PlhfW61aSncyX3Ml1oPDPokHTR7U8O2y" to R.drawable.ic_drive_quiver,
|
"https://drive.google.com/uc?export=view&id=1PlhfW61aSncyX3Ml1oPDPokHTR7U8O2y" to R.drawable.ic_drive_quiver,
|
||||||
"https://drive.google.com/uc?export=view&id=1WSpydN6AVjQrS6J_F8EFT00McQqCCxCw" to R.drawable.ic_drive_stake,
|
"https://drive.google.com/uc?export=view&id=1WSpydN6AVjQrS6J_F8EFT00McQqCCxCw" to R.drawable.ic_drive_stake,
|
||||||
"https://drive.google.com/uc?export=view&id=1ywDHw0C6exwxNCu-qNgQ0X2Sx9LfqSNH" to R.drawable.ic_drive_bird_skull,
|
"https://drive.google.com/uc?export=view&id=1ywDHw0C6exwxNCu-qNgQ0X2Sx9LfqSNH" to R.drawable.ic_drive_bird_skull,
|
||||||
|
|
|
||||||
BIN
app/src/main/res/drawable/ic_drive_bear_trap.webp
Normal file
BIN
app/src/main/res/drawable/ic_drive_bear_trap.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
app/src/main/res/drawable/ic_drive_chalk_stick.webp
Normal file
BIN
app/src/main/res/drawable/ic_drive_chalk_stick.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Loading…
Add table
Add a link
Reference in a new issue