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