Naive implementation to link inventory to firebase.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-07-16 11:06:09 +02:00
parent 531e4bea98
commit 464aea6fd6
26 changed files with 766 additions and 690 deletions

View file

@ -11,7 +11,7 @@ import com.pixelized.rplexicon.data.repository.character.AlterationRepository
import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository
import com.pixelized.rplexicon.data.repository.character.DescriptionRepository import com.pixelized.rplexicon.data.repository.character.DescriptionRepository
import com.pixelized.rplexicon.data.repository.character.EquipmentRepository import com.pixelized.rplexicon.data.repository.character.EquipmentRepository
import com.pixelized.rplexicon.data.repository.character.InventoryRepository import com.pixelized.rplexicon.data.repository.character.ItemsRepository
import com.pixelized.rplexicon.data.repository.character.ObjectActionRepository import com.pixelized.rplexicon.data.repository.character.ObjectActionRepository
import com.pixelized.rplexicon.data.repository.character.SkillRepository import com.pixelized.rplexicon.data.repository.character.SkillRepository
import com.pixelized.rplexicon.data.repository.character.SpellRepository import com.pixelized.rplexicon.data.repository.character.SpellRepository
@ -45,7 +45,7 @@ class LauncherViewModel @Inject constructor(
spellRepository: SpellRepository, spellRepository: SpellRepository,
skillRepository: SkillRepository, skillRepository: SkillRepository,
descriptionRepository: DescriptionRepository, descriptionRepository: DescriptionRepository,
inventoryRepository: InventoryRepository, itemsRepository: ItemsRepository,
equipmentRepository: EquipmentRepository, equipmentRepository: EquipmentRepository,
removeConRepository: RemoteConfigRepository // Unused but injected to initialize it. removeConRepository: RemoteConfigRepository // Unused but injected to initialize it.
) : ViewModel() { ) : ViewModel() {
@ -108,7 +108,7 @@ class LauncherViewModel @Inject constructor(
} }
val inventory = async { val inventory = async {
try { try {
inventoryRepository.fetchInventory() itemsRepository.fetchItems()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
_error.emit(FetchErrorUio.Structure(type = Type.INVENTORY)) _error.emit(FetchErrorUio.Structure(type = Type.INVENTORY))

View file

@ -1,53 +0,0 @@
package com.pixelized.rplexicon.data.model
data class Inventory(
val items: List<Item>,
) {
class Builder(
val items: MutableList<Item.Builder> = mutableListOf(),
) {
fun build(): Inventory = Inventory(
items = items.map { it.build() }
)
fun find(container: String): Item.Builder? {
return items.localFind { it.find(name = container) }
}
}
data class Item(
val name: String,
val amount: String?,
val items: List<Item>,
) {
class Builder(
var name: String,
var amount: String? = null,
val items: MutableList<Builder> = mutableListOf(),
) {
fun build(): Item = Item(
name = name,
amount = amount,
items = items.map { it.build() }
)
fun find(name: String): Builder? {
return when (this.name) {
name -> this
else -> items.localFind { it.find(name = name) }
}
}
}
}
}
private inline fun <T> Iterable<T>.localFind(predicate: (T) -> T?): T? {
var single: T? = null
for (element in this) {
single = predicate(element)
if (single != null) {
break
}
}
return single
}

View file

@ -0,0 +1,12 @@
package com.pixelized.rplexicon.data.model.item
import com.pixelized.rplexicon.data.model.roll.Throw
data class Item(
val id: String,
val prefix: String?,
val name: String,
val isContainer: Boolean,
val effect: Throw?,
val icon: Any?,
)

View file

@ -0,0 +1,17 @@
package com.pixelized.rplexicon.data.network
import androidx.annotation.Keep
import com.google.firebase.database.IgnoreExtraProperties
import com.google.firebase.database.PropertyName
@Keep
@IgnoreExtraProperties
class CharacterInventoryFire(
@get:PropertyName(INVENTORIES)
@set:PropertyName(INVENTORIES)
var inventories: Map<String, List<ItemDto>> = emptyMap(),
) {
companion object {
const val INVENTORIES = "Inventories"
}
}

View file

@ -25,7 +25,7 @@ data class CharacterSheetFire(
@get:PropertyName(ALTERATIONS) @get:PropertyName(ALTERATIONS)
@set:PropertyName(ALTERATIONS) @set:PropertyName(ALTERATIONS)
var alterations : Map<String, Boolean> = emptyMap() var alterations: Map<String, Boolean> = emptyMap(),
) { ) {
@Keep @Keep
@IgnoreExtraProperties @IgnoreExtraProperties

View file

@ -12,6 +12,6 @@ data class CharacterSheetFireMap(
var characters: Map<String, CharacterSheetFire> = emptyMap(), var characters: Map<String, CharacterSheetFire> = emptyMap(),
) { ) {
companion object { companion object {
private const val CHARACTERS = "Characters" const val CHARACTERS = "Characters"
} }
} }

View file

@ -0,0 +1,27 @@
package com.pixelized.rplexicon.data.network
import androidx.annotation.Keep
import com.google.firebase.database.IgnoreExtraProperties
import com.google.firebase.database.PropertyName
@Keep
@IgnoreExtraProperties
data class ItemDto(
@get:PropertyName(ID)
@set:PropertyName(ID)
var id: String? = null,
@get:PropertyName(AMOUNT)
@set:PropertyName(AMOUNT)
var amount: Int? = null,
@get:PropertyName(CHILDREN)
@set:PropertyName(CHILDREN)
var children: List<ItemDto> = emptyList(),
) {
companion object {
private const val ID = "id"
private const val AMOUNT = "amount"
private const val CHILDREN = "children"
}
}

View file

@ -1,55 +0,0 @@
package com.pixelized.rplexicon.data.parser.inventory
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.data.model.Inventory
import com.pixelized.rplexicon.data.parser.column
import com.pixelized.rplexicon.data.parser.parserScope
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import javax.inject.Inject
class InventoryParser @Inject constructor() {
@Throws(IncompatibleSheetStructure::class)
fun parse(sheet: ValueRange): Map<String, Inventory> = parserScope {
val inventories = hashMapOf<String, Inventory.Builder>()
sheet.forEachRowIndexed { index, row ->
when {
index == 0 -> updateStructure(row = row, columns = COLUMNS)
row.isNotEmpty() -> {
val character = row[0]?.toItem()
val container = row.parse(column = CONTAINER)
val name = row.parse(column = NAME)
val amount = row.parse(column = AMOUNT)
if (character != null && name != null) {
// retrieve the character inventory
val inventory = inventories.getOrPut(character) { Inventory.Builder() }
// make a item builder
val item = Inventory.Item.Builder(
name = name,
amount = amount,
)
// add the item to its container or by default to the inventory.
val isAdded = container?.let {
inventory.find(container = it)?.items?.add(item)
} ?: false
if (isAdded.not()) {
inventory.items.add(item)
}
}
}
}
}
return@parserScope inventories.mapValues { entry -> entry.value.build() }
}
companion object {
private val CONTAINER = column("Contenant")
private val NAME = column("Name")
private val AMOUNT = column("Quantité")
private val COLUMNS = listOf(CONTAINER, NAME, AMOUNT)
}
}

View file

@ -0,0 +1,56 @@
package com.pixelized.rplexicon.data.parser.inventory
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.data.model.item.Item
import com.pixelized.rplexicon.data.parser.column
import com.pixelized.rplexicon.data.parser.parserScope
import com.pixelized.rplexicon.data.parser.roll.ThrowParser
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.string.BaldurGageImageCache
import javax.inject.Inject
class ItemLexiconParser @Inject constructor(
private val throwParser: ThrowParser,
) {
@Throws(IncompatibleSheetStructure::class)
fun parse(sheet: ValueRange): Map<String, Item> = parserScope {
val inventories = hashMapOf<String, Item>()
sheet.forEachRowIndexed { index, row ->
when {
index == 0 -> updateStructure(row = row, columns = COLUMNS)
row.isNotEmpty() -> {
val container = row.parseBool(column = CONTAINER) ?: false
val id = row.parse(column = ID)
val name = row.parse(column = NAME)
if (id != null && name != null) {
val item = Item(
id = id,
prefix = row.parse(column = PREFIX),
name = name,
isContainer = container,
effect = throwParser.parse(value = row.parse(column = EFFECT)),
icon = BaldurGageImageCache.cache(url = row.parseUri(column = ICON)),
)
inventories[item.id] = item
}
}
}
}
return@parserScope inventories
}
companion object {
private val ID = column("Id")
private val PREFIX = column("Préfix")
private val NAME = column("Nom")
private val CONTAINER = column("Contenant")
private val EFFECT = column("Effet")
private val ICON = column("Icone")
private val COLUMNS = listOf(ID, PREFIX, NAME, CONTAINER, EFFECT, ICON)
}
}

View file

@ -19,9 +19,9 @@ object CharacterBinder {
const val MAGIC = "Magies" const val MAGIC = "Magies"
const val SKILL = "Capacités" const val SKILL = "Capacités"
const val MAGIC_LEXICON = "Lexique magique" const val MAGIC_LEXICON = "Lexique magique"
const val ITEMS_LEXICON = "Lexique des objets"
const val ALTERATION = "Altérations" const val ALTERATION = "Altérations"
const val DESCRIPTION = "Descriptions" const val DESCRIPTION = "Descriptions"
const val INVENTORY = "Inventaires"
const val EQUIPMENT = "Équipements" const val EQUIPMENT = "Équipements"
} }

View file

@ -1,7 +1,7 @@
package com.pixelized.rplexicon.data.repository.character package com.pixelized.rplexicon.data.repository.character
import com.pixelized.rplexicon.data.model.Inventory import com.pixelized.rplexicon.data.model.item.Item
import com.pixelized.rplexicon.data.parser.inventory.InventoryParser import com.pixelized.rplexicon.data.parser.inventory.ItemLexiconParser
import com.pixelized.rplexicon.data.repository.CharacterBinder import com.pixelized.rplexicon.data.repository.CharacterBinder
import com.pixelized.rplexicon.data.repository.GoogleSheetServiceRepository import com.pixelized.rplexicon.data.repository.GoogleSheetServiceRepository
import com.pixelized.rplexicon.utilitary.Update import com.pixelized.rplexicon.utilitary.Update
@ -12,23 +12,21 @@ import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class InventoryRepository @Inject constructor( class ItemsRepository @Inject constructor(
private val googleRepository: GoogleSheetServiceRepository, private val googleRepository: GoogleSheetServiceRepository,
private val inventoryParser: InventoryParser, private val itemLexiconParser: ItemLexiconParser,
) { ) {
private val _data = MutableStateFlow<Map<String, Inventory>>(emptyMap()) private val _data = MutableStateFlow<Map<String, Item>>(emptyMap())
val data: StateFlow<Map<String, Inventory>> get() = _data val data: StateFlow<Map<String, Item>> get() = _data
var lastSuccessFullUpdate: Update = Update.INITIAL var lastSuccessFullUpdate: Update = Update.INITIAL
private set private set
fun find(name: String?): Inventory? = _data.value[name]
@Throws(IncompatibleSheetStructure::class, Exception::class) @Throws(IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchInventory() { suspend fun fetchItems() {
googleRepository.fetch { sheet -> googleRepository.fetch { sheet ->
val request = sheet.get(CharacterBinder.ID, CharacterBinder.INVENTORY) val request = sheet.get(CharacterBinder.ID, CharacterBinder.ITEMS_LEXICON)
val data = inventoryParser.parse(sheet = request.execute()) val data = itemLexiconParser.parse(sheet = request.execute())
_data.tryEmit(data) _data.tryEmit(data)
lastSuccessFullUpdate = Update.currentTime() lastSuccessFullUpdate = Update.currentTime()
} }

View file

@ -9,11 +9,12 @@ import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.model.alteration.AlterationStatus import com.pixelized.rplexicon.data.model.alteration.AlterationStatus
import com.pixelized.rplexicon.data.network.CharacterInventoryFire
import com.pixelized.rplexicon.data.network.CharacterSheetFire import com.pixelized.rplexicon.data.network.CharacterSheetFire
import com.pixelized.rplexicon.data.network.CharacterSheetFireMap import com.pixelized.rplexicon.data.network.CharacterSheetFireMap
import com.pixelized.rplexicon.data.network.ItemDto
import com.pixelized.rplexicon.data.network.NetworkThrow import com.pixelized.rplexicon.data.network.NetworkThrow
import com.pixelized.rplexicon.data.network.NetworkThrowMap import com.pixelized.rplexicon.data.network.NetworkThrowMap
import com.pixelized.rplexicon.data.network.NetworkThrowMap.Companion.CHARACTERS_THROWS
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -74,6 +75,38 @@ class RealtimeDatabaseRepository @Inject constructor(
initialValue = emptyMap() initialValue = emptyMap()
) )
private val inventoryFire: StateFlow<Map<String, List<ItemDto>>> = callbackFlow {
// reference to the node
val reference = database.getReference("/")
// build a register the callback
val listener = reference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val value = try {
dataSnapshot.getValue(CharacterInventoryFire::class.java)
} catch (exception: Exception) {
Log.e(TAG, "Failed to parse value.", exception)
_error.tryEmit(exception)
null
}
if (value != null) {
trySend(value.inventories)
}
}
override fun onCancelled(error: DatabaseError) {
Log.e(TAG, "Failed to read value.", error.toException())
cancel()
}
})
awaitClose {
reference.removeEventListener(listener)
}
}.stateIn(
scope = CoroutineScope(Dispatchers.Default + Job()),
started = SharingStarted.Lazily,
initialValue = emptyMap()
)
private val networkThrows = callbackFlow { private val networkThrows = callbackFlow {
// reference to the node // reference to the node
val reference = database.getReference("/") val reference = database.getReference("/")
@ -133,6 +166,9 @@ class RealtimeDatabaseRepository @Inject constructor(
fun getCharacter(character: String): Flow<CharacterSheetFire> = fun getCharacter(character: String): Flow<CharacterSheetFire> =
characterFireSheet.mapNotNull { it[character] } characterFireSheet.mapNotNull { it[character] }
fun getInventory(character: String): Flow<List<ItemDto>> =
inventoryFire.map { it[character] ?: emptyList() }
fun getThrows(): Flow<NetworkThrowMap> = fun getThrows(): Flow<NetworkThrowMap> =
networkThrows networkThrows
@ -193,6 +229,13 @@ class RealtimeDatabaseRepository @Inject constructor(
reference.setValue(value.value) reference.setValue(value.value)
} }
fun setInventory(character: String?, inventory: List<ItemDto>) {
character?.let {
val reference = database.getReference("$PATH_INVENTORY/")
reference.updateChildren(mapOf(it to inventory))
}
}
fun sendThrow(character: String?, throws: NetworkThrow) { fun sendThrow(character: String?, throws: NetworkThrow) {
character?.let { character?.let {
val reference = database.getReference("$PATH_THROWS/") val reference = database.getReference("$PATH_THROWS/")
@ -202,7 +245,8 @@ class RealtimeDatabaseRepository @Inject constructor(
companion object { companion object {
private const val TAG = "FirebaseRepository" private const val TAG = "FirebaseRepository"
private const val PATH_CHARACTERS = "Characters" private const val PATH_CHARACTERS = CharacterSheetFireMap.CHARACTERS
private const val PATH_THROWS = CHARACTERS_THROWS private const val PATH_THROWS = NetworkThrowMap.CHARACTERS_THROWS
private const val PATH_INVENTORY = CharacterInventoryFire.INVENTORIES
} }
} }

View file

@ -71,6 +71,8 @@ import com.pixelized.rplexicon.ui.screens.character.CharacterTabUio.Proficiency
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeader import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeader
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.character.ResourcePointUio import com.pixelized.rplexicon.ui.screens.character.composable.character.ResourcePointUio
import com.pixelized.rplexicon.ui.screens.character.composable.chooser.SpellLevelChooser
import com.pixelized.rplexicon.ui.screens.character.composable.chooser.SpellLevelChooserPreview
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPage import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPage
import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPagePreview import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPagePreview
@ -82,8 +84,6 @@ import com.pixelized.rplexicon.ui.screens.character.pages.actions.SpellsViewMode
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPage import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPage
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPagePreview import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationPagePreview
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationViewModel import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationViewModel
import com.pixelized.rplexicon.ui.screens.character.composable.chooser.SpellLevelChooser
import com.pixelized.rplexicon.ui.screens.character.composable.chooser.SpellLevelChooserPreview
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryPage import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryPage
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryPagePreview import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryPagePreview
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryViewModel import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryViewModel
@ -208,7 +208,12 @@ fun CharacterSheetScreen(
onLevel = { spell, level -> onLevel = { spell, level ->
scope.launch { scope.launch {
sheetState.hide() sheetState.hide()
overlay.prepareRoll(diceThrow = spellsViewModel.onCastSpell(spell, level)) overlay.prepareRoll(
diceThrow = spellsViewModel.onCastSpell(
spell,
level
)
)
overlay.showOverlay() overlay.showOverlay()
} }
}, },
@ -433,12 +438,7 @@ private fun rememberHeaderTabsState(
else -> emptyList() else -> emptyList()
} }
) )
addAll( add(Inventory)
when {
inventoryViewModel.inventory.value.isNotEmpty() -> listOf(Inventory)
else -> emptyList()
}
)
} }
} }
} }

View file

@ -12,7 +12,7 @@ import com.pixelized.rplexicon.data.repository.character.AlterationRepository
import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository
import com.pixelized.rplexicon.data.repository.character.DescriptionRepository import com.pixelized.rplexicon.data.repository.character.DescriptionRepository
import com.pixelized.rplexicon.data.repository.character.EquipmentRepository import com.pixelized.rplexicon.data.repository.character.EquipmentRepository
import com.pixelized.rplexicon.data.repository.character.InventoryRepository import com.pixelized.rplexicon.data.repository.character.ItemsRepository
import com.pixelized.rplexicon.data.repository.character.ObjectActionRepository import com.pixelized.rplexicon.data.repository.character.ObjectActionRepository
import com.pixelized.rplexicon.data.repository.character.SkillRepository import com.pixelized.rplexicon.data.repository.character.SkillRepository
import com.pixelized.rplexicon.data.repository.character.SpellRepository import com.pixelized.rplexicon.data.repository.character.SpellRepository
@ -33,7 +33,7 @@ class CharacterSheetViewModel @Inject constructor(
private val characterRepository: CharacterSheetRepository, private val characterRepository: CharacterSheetRepository,
private val descriptionRepository: DescriptionRepository, private val descriptionRepository: DescriptionRepository,
private val alterationRepository: AlterationRepository, private val alterationRepository: AlterationRepository,
private val inventoryRepository: InventoryRepository, private val itemsRepository: ItemsRepository,
private val equipmentRepository: EquipmentRepository, private val equipmentRepository: EquipmentRepository,
private val actionRepository: ActionRepository, private val actionRepository: ActionRepository,
private val objectRepository: ObjectActionRepository, private val objectRepository: ObjectActionRepository,
@ -86,9 +86,9 @@ class CharacterSheetViewModel @Inject constructor(
} }
} }
val inventory = async { val inventory = async {
if (force || inventoryRepository.lastSuccessFullUpdate.shouldUpdate()) { if (force || itemsRepository.lastSuccessFullUpdate.shouldUpdate()) {
try { try {
inventoryRepository.fetchInventory() itemsRepository.fetchItems()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
_error.emit(FetchErrorUio.Structure(type = Type.INVENTORY)) _error.emit(FetchErrorUio.Structure(type = Type.INVENTORY))

View file

@ -1,127 +0,0 @@
package com.pixelized.rplexicon.ui.screens.character.composable.actions
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Divider
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberInventoryListState
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.rememberTextSize
@Stable
data class InventoryItemUio(
val name: String,
val amount: String? = null,
val items: List<InventoryItemUio> = emptyList(),
)
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun InventoryItem(
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 2.dp),
item: InventoryItemUio,
) {
Column(
modifier = Modifier
.padding(paddingValues = padding)
.then(other = modifier),
) {
FlowRow {
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.bodyMedium,
text = item.name,
)
item.amount?.let {
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Medium,
style = MaterialTheme.typography.labelLarge,
text = " : ",
)
Text(
modifier = Modifier.alignByBaseline(),
fontWeight = FontWeight.Light,
style = MaterialTheme.typography.bodyMedium,
text = it
)
}
}
val lastIndex = remember(item.items.size) { item.items.lastIndex }
item.items.forEachIndexed { index, item ->
Row(
modifier = Modifier.height(intrinsicSize = IntrinsicSize.Min),
) {
val size = rememberTextSize(style = MaterialTheme.typography.bodyMedium)
if (index == lastIndex) {
Box(
modifier = Modifier
.height(height = 3.dp + size.height / 2)
.width(1.dp)
.background(color = DividerDefaults.color)
)
} else {
Box(
modifier = Modifier
.fillMaxHeight()
.width(1.dp)
.background(color = DividerDefaults.color)
)
}
Box(
modifier = Modifier
.padding(top = 2.dp + size.height / 2)
.height(1.dp)
.width(8.dp)
.background(color = DividerDefaults.color)
)
InventoryItem(
padding = PaddingValues(start = 7.dp, top = 2.dp, bottom = 2.dp),
item = item,
)
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun InventoryItemPreview() {
LexiconTheme {
Surface {
Column {
val items = rememberInventoryListState()
items.value.forEach {
InventoryItem(
item = it,
)
}
}
}
}
}

View file

@ -5,37 +5,114 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import com.pixelized.rplexicon.ui.screens.character.composable.actions.InventoryItemUio import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryItemUio
@Composable @Composable
@Stable @Stable
fun rememberInventoryListState(): State<List<InventoryItemUio>> { fun rememberInventoryListState(): State<List<InventoryItemUio>> {
return remember { return remember {
var id = 0
mutableStateOf( mutableStateOf(
listOf( listOf(
InventoryItemUio( InventoryItemUio(
name = "Bourse", id = "${id++}",
items = listOf( name = "Pouch",
InventoryItemUio(name = "Or", amount = "21"), amount = 1,
), container = true,
icon = R.drawable.icbg_pouch_a_unfaded,
), ),
InventoryItemUio( InventoryItemUio(
name = "Sac à dos", id = "${id++}",
items = listOf( name = "Backpack",
InventoryItemUio(name = "Sac de couchage"), amount = 1,
InventoryItemUio(name = "Kit de cuisine"), container = true,
InventoryItemUio(name = "Boite d'allume-feu"), icon = R.drawable.icbg_backpack_a_unfaded,
InventoryItemUio(name = "Torches", amount = "10"), ),
InventoryItemUio(name = "Rations journalières", amount = "10"), InventoryItemUio(
InventoryItemUio(name = "Outre d'eau"), id = "${id++}",
InventoryItemUio(name = "Cordes", amount = "15 mètres"), name = "Scroll of blessing",
InventoryItemUio(name = "Piège de chasse"), amount = 1,
InventoryItemUio(name = "Bâton de marche"), container = false,
), icon = R.drawable.icbg_scroll_of_bless_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Scroll of spirit weapon",
amount = 1,
container = false,
icon = R.drawable.icbg_book_signedtradebisa_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Potion of healing",
amount = 2,
container = false,
icon = R.drawable.icbg_potion_of_superior_healing_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Potion of supérior healing",
amount = 2,
container = false,
icon = R.drawable.icbg_pot_potion_of_healing_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Potion of holy water",
amount = 2,
container = false,
icon = R.drawable.icbg_grn_holy_water_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Leather armor",
amount = 1,
container = false,
icon = R.drawable.icbg_leather_armour_rogue_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Silver battleaxe",
amount = 1,
container = false,
icon = R.drawable.icbg_battleaxe_plus_one_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Hand crossbow",
amount = 1,
container = false,
icon = R.drawable.icbg_hand_crossbow_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Goodberry",
amount = 1,
container = false,
icon = R.drawable.icbg_food_goodberry_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Goodberry",
amount = 1,
container = false,
icon = R.drawable.icbg_worg_fang_unfaded,
),
InventoryItemUio(
id = "${id++}",
name = "Lantern of revealing",
amount = 1,
container = false,
icon = R.drawable.icbg_lantern_of_revealing,
),
InventoryItemUio(
id = "${id++}",
name = "Dust of disappearance",
amount = 1,
container = false,
icon = R.drawable.icbg_haste_spore_grenade_unfaded,
), ),
InventoryItemUio(name = "Dague"),
InventoryItemUio(name = "Javelot", amount = "4"),
InventoryItemUio(name = "Cape de protection"),
) )
) )
} }

View file

@ -1,16 +1,29 @@
package com.pixelized.rplexicon.ui.screens.character.factory package com.pixelized.rplexicon.ui.screens.character.factory
import com.pixelized.rplexicon.data.model.Inventory import com.pixelized.rplexicon.data.model.item.Item
import com.pixelized.rplexicon.ui.screens.character.composable.actions.InventoryItemUio import com.pixelized.rplexicon.data.network.ItemDto
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryItemUio
import javax.inject.Inject import javax.inject.Inject
class ItemUioFactory @Inject constructor() { class ItemUioFactory @Inject constructor() {
fun toUio(item: Inventory.Item): InventoryItemUio { fun toUio(
return InventoryItemUio( items: Map<String, Item>,
name = item.name, fires: List<ItemDto>,
amount = item.amount, ): List<InventoryItemUio> {
items = item.items.map { toUio(it) } return fires.mapNotNull { fire ->
) items[fire.id]?.let { item ->
fire.amount?.let { amount ->
InventoryItemUio(
id = item.id,
name = item.name,
amount = amount,
container = item.isContainer,
icon = item.icon,
items = null // fire.children?.let { toUio(items = items, fires = it) },
)
}
}
}
} }
} }

View file

@ -34,12 +34,12 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable @Stable
data class InventoryItemUio( data class InventoryItemUio(
val id: Int, val id: String,
val name: String, val name: String,
val amount: Int, val amount: Int = 1,
val container: Boolean, val container: Boolean,
val icon: Any?, val icon: Any? = R.drawable.icbg_generic_darkness_icon,
val items: List<InventoryItemUio> = emptyList(), val items: List<InventoryItemUio>? = null,
) )
@Composable @Composable
@ -57,6 +57,39 @@ fun InventoryItem(
model = item.icon, model = item.icon,
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
) )
AnimatedContent(
modifier = Modifier
.align(alignment = Alignment.TopEnd)
.offset(x = 0.dp, y = (-4).dp)
.padding(horizontal = 2.dp),
targetState = item.items?.size,
transitionSpec = {
// Compare the incoming number with the previous number.
if ((targetState ?: 0) > (initialState ?: 0)) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height / 2 } + fadeIn() togetherWith slideOutVertically { height -> -height / 2 } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height / 2 } + fadeIn() togetherWith slideOutVertically { height -> height / 2 } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
},
label = "Container count size",
) { amount ->
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.End,
text = amount.takeIf { it != null }?.let { "$it" } ?: "",
)
}
AnimatedContent( AnimatedContent(
modifier = Modifier modifier = Modifier
.align(alignment = Alignment.BottomEnd) .align(alignment = Alignment.BottomEnd)
@ -68,13 +101,11 @@ fun InventoryItem(
if (targetState > initialState) { if (targetState > initialState) {
// If the target number is larger, it slides up and fades in // If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out. // while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height / 2 } + fadeIn() togetherWith slideInVertically { height -> height / 2 } + fadeIn() togetherWith slideOutVertically { height -> -height / 2 } + fadeOut()
slideOutVertically { height -> -height / 2 } + fadeOut()
} else { } else {
// If the target number is smaller, it slides down and fades in // If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out. // while the initial number slides down and fades out.
slideInVertically { height -> -height / 2 } + fadeIn() togetherWith slideInVertically { height -> -height / 2 } + fadeIn() togetherWith slideOutVertically { height -> height / 2 } + fadeOut()
slideOutVertically { height -> height / 2 } + fadeOut()
}.using( }.using(
// Disable clipping since the faded slide-in/out should // Disable clipping since the faded slide-in/out should
// be displayed out of bounds. // be displayed out of bounds.
@ -90,42 +121,7 @@ fun InventoryItem(
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
textAlign = TextAlign.End, textAlign = TextAlign.End,
text = amount.takeIf { it > 0 }?.let { "$it" } ?: " ", // &nbsp; text = amount.takeIf { it > 1 }?.let { "$it" } ?: " ", // &nbsp;
)
}
AnimatedContent(
modifier = Modifier
.align(alignment = Alignment.TopEnd)
.offset(x = 0.dp, y = (-4).dp)
.padding(horizontal = 2.dp),
targetState = item.items.size,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height / 2 } + fadeIn() togetherWith
slideOutVertically { height -> -height / 2 } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height / 2 } + fadeIn() togetherWith
slideOutVertically { height -> height / 2 } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
},
label = "Container count size",
) { amount ->
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.End,
text = amount.takeIf { it > 0 }?.let { "$it" } ?: " ", // &nbsp;
) )
} }
} }
@ -150,23 +146,21 @@ private fun InventoryItemPreview(
private class ClassInventoryItemProvider : PreviewParameterProvider<InventoryItemUio> { private class ClassInventoryItemProvider : PreviewParameterProvider<InventoryItemUio> {
override val values: Sequence<InventoryItemUio> = sequenceOf( override val values: Sequence<InventoryItemUio> = sequenceOf(
InventoryItemUio( InventoryItemUio(
id = 0, id = "0",
name = "Pouch", name = "Pouch",
amount = 1, amount = 1,
container = true, container = true,
icon = R.drawable.icbg_pouch_a_unfaded, icon = R.drawable.icbg_pouch_a_unfaded,
),
),
InventoryItemUio( InventoryItemUio(
id = 1, id = "1",
name = "Scroll of blessing", name = "Scroll of blessing",
amount = 1, amount = 1,
container = false, container = false,
icon = R.drawable.icbg_scroll_of_bless_unfaded, icon = R.drawable.icbg_scroll_of_bless_unfaded,
),
),
InventoryItemUio( InventoryItemUio(
id = 2, id = "2",
name = "Potion of blessing", name = "Potion of blessing",
amount = 2, amount = 2,
container = false, container = false,

View file

@ -2,13 +2,22 @@ package com.pixelized.rplexicon.ui.screens.character.pages.inventory
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.animation.animateColor
import androidx.compose.animation.core.updateTransition
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.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
@ -16,19 +25,23 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.compositeOver
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalView
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
import com.pixelized.rplexicon.LocalSnack import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.screens.character.composable.actions.EquipmentItem import com.pixelized.rplexicon.ui.screens.character.composable.actions.EquipmentItem
import com.pixelized.rplexicon.ui.screens.character.composable.actions.EquipmentItemUio import com.pixelized.rplexicon.ui.screens.character.composable.actions.EquipmentItemUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.GenericHeader import com.pixelized.rplexicon.ui.screens.character.composable.dialogs.SkillDetailDialog
import com.pixelized.rplexicon.ui.screens.character.composable.actions.InventoryItem
import com.pixelized.rplexicon.ui.screens.character.composable.actions.InventoryItemUio
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberEquipmentState import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberEquipmentState
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberInventoryListState import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberInventoryListState
import com.pixelized.rplexicon.ui.screens.character.composable.dialogs.SkillDetailDialog
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.DraggableItem
import com.pixelized.rplexicon.utilitary.dragContainer
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import com.pixelized.rplexicon.utilitary.rememberGridDragDropState
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -36,6 +49,7 @@ import kotlinx.coroutines.launch
fun InventoryPage( fun InventoryPage(
viewModel: InventoryViewModel, viewModel: InventoryViewModel,
) { ) {
val view = LocalView.current
val snack = LocalSnack.current val snack = LocalSnack.current
val snackJob = remember { mutableStateOf<Job?>(null) } val snackJob = remember { mutableStateOf<Job?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -48,6 +62,10 @@ fun InventoryPage(
snackJob.value?.cancel() snackJob.value?.cancel()
viewModel.showSkillDetailDialog(item = it) viewModel.showSkillDetailDialog(item = it)
}, },
onInventoryItemMove = viewModel::onMove,
onInventoryIsMoveEnable = viewModel::isMoveEnable,
onInventoryItemDrop = viewModel::onDrop,
onInventoryItemDropOver = viewModel::onDropOver,
) )
SkillDetailDialog( SkillDetailDialog(
@ -65,37 +83,102 @@ fun InventoryPage(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun InventoryPageContent( private fun InventoryPageContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
equipments: State<EquipmentItemUio?>, equipments: State<EquipmentItemUio?>,
inventory: State<List<InventoryItemUio>>, inventory: State<List<InventoryItemUio>>,
onEquipment: (String) -> Unit, onEquipment: (String) -> Unit,
onInventoryItemMove: (Int, Int) -> Unit,
onInventoryIsMoveEnable: (Int, Int) -> Boolean,
onInventoryItemDrop: (Int) -> Unit,
onInventoryItemDropOver: (Int, Int) -> Unit,
) { ) {
LazyColumn( val contentPadding = remember { PaddingValues(16.dp) }
val gridState = rememberLazyGridState()
val dragDropState = rememberGridDragDropState(
contentPadding = contentPadding,
gridState = gridState,
onMove = onInventoryItemMove,
isMoveEnable = onInventoryIsMoveEnable,
onDragRelease = onInventoryItemDrop,
onDropReleaseOver = onInventoryItemDropOver,
)
Column(
modifier = modifier, modifier = modifier,
contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp),
) { ) {
equipments.value?.let { equipments.value?.let {
item { EquipmentItem(
EquipmentItem( modifier = Modifier.padding(bottom = 16.dp),
modifier = Modifier.padding(bottom = 16.dp), equipments = it,
equipments = it, onClick = onEquipment,
onClick = onEquipment, )
)
}
} }
if (inventory.value.isNotEmpty()) { LazyVerticalGrid(
stickyHeader { columns = GridCells.Fixed(5),
GenericHeader( modifier = Modifier
label = R.string.character_sheet_title_inventory .graphicsLayer { this.clip = false }
) .dragContainer(dragDropState)
} .fillMaxSize(),
items(items = inventory.value) { state = gridState,
InventoryItem( contentPadding = contentPadding,
item = it, verticalArrangement = Arrangement.spacedBy(8.dp),
) horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(
items = inventory.value,
key = { _, item -> item.id },
) { index, item ->
DraggableItem(
dragDropState = dragDropState,
index = index
) { isDragging, isOvering ->
val colorScheme = MaterialTheme.lexicon.colorScheme
val transition = updateTransition(
targetState = isDragging || isOvering,
label = "Dragging transition",
)
val backgroundColor = transition.animateColor(
label = "Draggable item background color",
) {
val surface = colorScheme.base.surfaceColorAtElevation(2.dp)
when (it) {
true -> colorScheme.base.primary.copy(alpha = 0.15f)
.compositeOver(surface)
else -> surface
}
}
val outlineColor = transition.animateColor(
label = "Draggable item outline color",
) {
when (it) {
true -> colorScheme.base.primary
else -> colorScheme.characterSheet.outlineBorder
}
}
val innerColor = transition.animateColor(
label = "Draggable item inline color",
) {
when (it) {
true -> colorScheme.base.primary
else -> colorScheme.characterSheet.innerBorder
}
}
Box(
modifier = Modifier.doubleBorder(
backgroundColor = backgroundColor.value,
outline = remember { RoundedCornerShape(8.dp) },
outlineColor = outlineColor.value,
inner = remember { RoundedCornerShape(6.dp) },
innerColor = innerColor.value,
)
) {
InventoryItem(
item = item,
)
}
}
} }
} }
} }
@ -112,6 +195,10 @@ fun InventoryPagePreview() {
equipments = rememberEquipmentState(), equipments = rememberEquipmentState(),
inventory = rememberInventoryListState(), inventory = rememberInventoryListState(),
onEquipment = { }, onEquipment = { },
onInventoryItemMove = { _, _ -> },
onInventoryIsMoveEnable = { _, _ -> true },
onInventoryItemDrop = { _ -> },
onInventoryItemDropOver = { _, _ -> }
) )
} }
} }

View file

@ -1,18 +1,23 @@
package com.pixelized.rplexicon.ui.screens.character.pages.inventory package com.pixelized.rplexicon.ui.screens.character.pages.inventory
import android.app.Application import android.app.Application
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.model.item.Item
import com.pixelized.rplexicon.data.network.ItemDto
import com.pixelized.rplexicon.data.repository.character.DescriptionRepository import com.pixelized.rplexicon.data.repository.character.DescriptionRepository
import com.pixelized.rplexicon.data.repository.character.EquipmentRepository import com.pixelized.rplexicon.data.repository.character.EquipmentRepository
import com.pixelized.rplexicon.data.repository.character.InventoryRepository import com.pixelized.rplexicon.data.repository.character.ItemsRepository
import com.pixelized.rplexicon.data.repository.firebase.RealtimeDatabaseRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument import com.pixelized.rplexicon.ui.navigation.screens.characterSheetArgument
import com.pixelized.rplexicon.ui.screens.character.composable.actions.EquipmentItemUio import com.pixelized.rplexicon.ui.screens.character.composable.actions.EquipmentItemUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.InventoryItemUio
import com.pixelized.rplexicon.ui.screens.character.composable.dialogs.SkillDialogDetailUio import com.pixelized.rplexicon.ui.screens.character.composable.dialogs.SkillDialogDetailUio
import com.pixelized.rplexicon.ui.screens.character.factory.ItemUioFactory import com.pixelized.rplexicon.ui.screens.character.factory.ItemUioFactory
import com.pixelized.rplexicon.utilitary.extentions.context import com.pixelized.rplexicon.utilitary.extentions.context
@ -21,17 +26,23 @@ import com.pixelized.rplexicon.utilitary.extentions.uri
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class InventoryViewModel @Inject constructor( class InventoryViewModel @Inject constructor(
private val inventoryRepository: InventoryRepository,
private val equipmentRepository: EquipmentRepository, private val equipmentRepository: EquipmentRepository,
private val descriptionRepository: DescriptionRepository, private val descriptionRepository: DescriptionRepository,
private val fireRepository: RealtimeDatabaseRepository,
private val itemFactory: ItemUioFactory, private val itemFactory: ItemUioFactory,
itemsRepository: ItemsRepository,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
application: Application application: Application
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
@ -40,8 +51,55 @@ class InventoryViewModel @Inject constructor(
private val _equipments = mutableStateOf<EquipmentItemUio?>(null) private val _equipments = mutableStateOf<EquipmentItemUio?>(null)
val equipments: State<EquipmentItemUio?> get() = _equipments val equipments: State<EquipmentItemUio?> get() = _equipments
private val _inventory = mutableStateOf<List<InventoryItemUio>>(emptyList()) // Target the local indexes form specific ids (change locally to avoid flooding of firebase)
val inventory: State<List<InventoryItemUio>> get() = _inventory private val itemLocalIndex =
MutableStateFlow<Map<String, Int>>(hashMapOf())
// inversion of itemLocalIndex
private val itemLocalId =
itemLocalIndex.map { items -> items.map { it.value to it.key }.toMap() }.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyMap(),
)
// inventory for a specific character in firebase.
private val fireInventory = fireRepository
.getInventory(character = character)
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
// UI data, merge the firebase inventory full data and with the local index and transform it into UIO.
private val _inventory = itemsRepository.data
.combine(fireInventory) { items, fire ->
Data().also {
it.items = items
it.fire = fire
}
}
.combine(itemLocalIndex) { data, indexes ->
data.also {
it.indexes = indexes
}
}
.map { data ->
val (items, fire, indexes) = data
itemFactory.toUio(items = items, fires = fire).sortedBy { indexes[it.id] }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList(),
)
val inventory: State<List<InventoryItemUio>>
@Composable
@Stable
get() {
return _inventory.collectAsState(emptyList())
}
private val _dialog = mutableStateOf<SkillDialogDetailUio?>(null) private val _dialog = mutableStateOf<SkillDialogDetailUio?>(null)
val dialog: State<SkillDialogDetailUio?> get() = _dialog val dialog: State<SkillDialogDetailUio?> get() = _dialog
@ -52,11 +110,14 @@ class InventoryViewModel @Inject constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
inventoryRepository.data.collect { inventories -> fireRepository.getInventory(character).collect { items ->
val items = inventories[character]?.items?.map { itemFactory.toUio(it) } itemLocalIndex.value = items.mapNotNull {
withContext(Dispatchers.Main) { if (it.id != null) {
_inventory.value = items ?: emptyList() it.id as String to items.indexOf(it)
} } else {
null
}
}.toMap()
} }
} }
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
@ -85,6 +146,40 @@ class InventoryViewModel @Inject constructor(
} }
} }
} }
launch(Dispatchers.IO) {
fireRepository.setInventory(
character = character,
inventory = listOf(
ItemDto(
id = "7d27561b-f2f4-4899-a2fc-df3501b1b66b",
amount = 1,
children = listOf(
ItemDto(
id = "bd1400bf-9d1c-480c-873b-0539ac82bfdb",
amount = 32,
),
ItemDto(
id = "85f778ae-11c7-47a9-bddc-f6d8a1fd03dc",
amount = 5,
),
ItemDto(
id = "950d9a2b-fc1a-4989-bb92-930de98f7ea4",
amount = 15,
),
),
),
ItemDto(
id = "1c69559c-b96f-4600-99b3-85c07cb528e0",
amount = 1,
),
ItemDto(
id = "1a5931c6-bb45-43ea-ad25-207aac820383",
amount = 1,
),
)
)
}
} }
} }
@ -109,4 +204,75 @@ class InventoryViewModel @Inject constructor(
fun hideSkillDetailDialog() { fun hideSkillDetailDialog() {
_dialog.value = null _dialog.value = null
} }
fun onMove(fromIndex: Int, toIndex: Int) {
// item have been moved, need to update the local item index.
itemLocalIndex.value = itemLocalIndex.value.toMutableMap().also { map ->
val fromId = map.firstNotNullOfOrNull { if (it.value == fromIndex) it.key else null }
val toId = map.firstNotNullOfOrNull { if (it.value == toIndex) it.key else null }
if (fromId != null && toId != null) {
map[fromId] = toIndex
map[toId] = fromIndex
}
}
}
fun isMoveEnable(fromIndex: Int, toIndex: Int): Boolean {
val receiver = _inventory.value[toIndex]
return !receiver.container
}
fun onDrop(index: Int) {
val source = _inventory.value[index]
val fireIndex = fireInventory.value.indexOfFirst { it.id == source.id }
val inventory = fireInventory.value.toMutableList().also { fireInventory ->
fireInventory.add(index, fireInventory.removeAt(fireIndex))
}
fireRepository.setInventory(
character = character,
inventory = inventory,
)
}
fun onDropOver(fromIndex: Int, toIndex: Int) {
val source = _inventory.value[fromIndex]
val receiver = _inventory.value[toIndex]
if (fromIndex != toIndex && receiver.container) {
val inventory = fireInventory.value.toMutableList().also { fireInventory ->
val sourceIndex = fireInventory.indexOfFirst { it.id == source.id }
val receiverIndex = fireInventory.indexOfFirst { it.id == receiver.id }
val sourceFireItem = fireInventory.getOrNull(sourceIndex)
val receiverFireItem = fireInventory.getOrNull(receiverIndex)
if (sourceFireItem != null && receiverFireItem != null) {
fireInventory.remove(sourceFireItem)
fireInventory.remove(receiverFireItem)
fireInventory.add(
receiverIndex,
receiverFireItem.copy(
children = receiverFireItem.children.toMutableList()
.also { it.add(sourceFireItem) },
)
)
}
}
fireRepository.setInventory(
character = character,
inventory = inventory,
)
}
}
private class Data {
lateinit var items: Map<String, Item>
lateinit var fire: List<ItemDto>
lateinit var indexes: Map<String, Int>
operator fun component1() = items
operator fun component2() = fire
operator fun component3() = indexes
}
} }

View file

@ -1,254 +0,0 @@
package com.pixelized.rplexicon.ui.screens.character.pages.inventory
import android.content.res.Configuration
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.DraggableItem
import com.pixelized.rplexicon.utilitary.dragContainer
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.doubleBorder
import com.pixelized.rplexicon.utilitary.rememberGridDragDropState
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
fun Pouet() {
val items = remember {
mutableStateOf(
listOf(
InventoryItemUio(
id = 0,
name = "Pouch",
amount = 1,
container = true,
icon = R.drawable.icbg_pouch_a_unfaded,
),
InventoryItemUio(
id = 1,
name = "Scroll of blessing",
amount = 1,
container = false,
icon = R.drawable.icbg_scroll_of_bless_unfaded,
),
InventoryItemUio(
id = 2,
name = "Scroll of spirit weapon",
amount = 1,
container = false,
icon = R.drawable.icbg_book_signedtradebisa_unfaded,
),
InventoryItemUio(
id = 3,
name = "Potion of healing",
amount = 2,
container = false,
icon = R.drawable.icbg_potion_of_superior_healing_unfaded,
),
InventoryItemUio(
id = 4,
name = "Potion of supérior healing",
amount = 2,
container = false,
icon = R.drawable.icbg_pot_potion_of_healing_unfaded,
),
InventoryItemUio(
id = 5,
name = "Potion of holy water",
amount = 2,
container = false,
icon = R.drawable.icbg_grn_holy_water_unfaded,
),
InventoryItemUio(
id = 6,
name = "Leather armor",
amount = 1,
container = false,
icon = R.drawable.icbg_leather_armour_rogue_unfaded,
),
InventoryItemUio(
id = 7,
name = "Silver battleaxe",
amount = 0,
container = false,
icon = R.drawable.icbg_battleaxe_plus_one_unfaded,
),
InventoryItemUio(
id = 8,
name = "Hand crossbow",
amount = 0,
container = false,
icon = R.drawable.icbg_hand_crossbow_unfaded,
),
InventoryItemUio(
id = 9,
name = "Goodberry",
amount = 0,
container = false,
icon = R.drawable.icbg_food_goodberry_unfaded,
),
InventoryItemUio(
id = 10,
name = "Goodberry",
amount = 0,
container = false,
icon = R.drawable.icbg_worg_fang_unfaded,
),
InventoryItemUio(
id = 11,
name = "Lantern of revealing",
amount = 0,
container = false,
icon = R.drawable.icbg_lantern_of_revealing,
),
InventoryItemUio(
id = 12,
name = "Dust of disappearance",
amount = 0,
container = false,
icon = R.drawable.icbg_haste_spore_grenade_unfaded,
),
InventoryItemUio(
id = 13,
name = "Pouch",
amount = 1,
container = true,
icon = R.drawable.icbg_pouch_a_unfaded,
),
)
)
}
val overIndex = remember { mutableIntStateOf(-1) }
val contentPadding = remember { PaddingValues(16.dp) }
val gridState = rememberLazyGridState()
val dragDropState = rememberGridDragDropState(
contentPadding = contentPadding,
gridState = gridState,
onMove = { fromIndex, toIndex ->
items.value = items.value.toMutableList().apply {
add(toIndex, removeAt(fromIndex))
}
},
onOver = { _, toIndex ->
val receiver = items.value[toIndex]
if (receiver.container) {
overIndex.intValue = toIndex
false
} else {
true
}
},
onDrop = { fromIndex, toIndex ->
if (fromIndex != toIndex) {
val receiver = items.value[toIndex]
if (receiver.container) {
items.value = items.value.toMutableList().apply {
val item = removeAt(fromIndex)
val receiverCopy = receiver.copy(
items = receiver.items.toMutableList().also {
it.add(item)
}
)
val receiverIndex = indexOf(receiver)
removeAt(receiverIndex)
add(receiverIndex, receiverCopy)
}
}
}
}
)
LexiconTheme {
Surface {
LazyVerticalGrid(
columns = GridCells.Fixed(5),
modifier = Modifier
.dragContainer(dragDropState)
.fillMaxSize(),
state = gridState,
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(
items = items.value,
key = { _, item -> item.id },
) { index, item ->
DraggableItem(
dragDropState = dragDropState,
index = index
) { isDragging, isOvering ->
val colorScheme = MaterialTheme.lexicon.colorScheme
val transition = updateTransition(
targetState = isDragging || isOvering ,
label = "Dragging transition",
)
val backgroundColor = transition.animateColor(
label = "Draggable item background color",
) {
val surface = colorScheme.base.surfaceColorAtElevation(2.dp)
when (it) {
true -> colorScheme.base.primary.copy(alpha = 0.15f)
.compositeOver(surface)
else -> surface
}
}
val outlineColor = transition.animateColor(
label = "Draggable item outline color",
) {
when (it) {
true -> colorScheme.base.primary
else -> colorScheme.characterSheet.outlineBorder
}
}
val innerColor = transition.animateColor(
label = "Draggable item inline color",
) {
when (it) {
true -> colorScheme.base.primary
else -> colorScheme.characterSheet.innerBorder
}
}
Box(
modifier = Modifier.doubleBorder(
backgroundColor = backgroundColor.value,
outline = remember { RoundedCornerShape(8.dp) },
outlineColor = outlineColor.value,
inner = remember { RoundedCornerShape(6.dp) },
innerColor = innerColor.value,
)
) {
InventoryItem(
item = item,
)
}
}
}
}
}
}
}

View file

@ -47,15 +47,17 @@ fun rememberGridDragDropState(
contentPadding: PaddingValues, contentPadding: PaddingValues,
gridState: LazyGridState, gridState: LazyGridState,
onMove: (Int, Int) -> Unit, onMove: (Int, Int) -> Unit,
onOver: (Int, Int) -> Boolean, isMoveEnable: (Int, Int) -> Boolean,
onDrop: (Int, Int) -> Unit, onDragRelease: (Int) -> Unit,
onDropReleaseOver: (Int, Int) -> Unit,
): GridDragDropState { ): GridDragDropState {
val density = LocalDensity.current val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val currentOnMove = rememberUpdatedState(newValue = onMove) val currentOnMove = rememberUpdatedState(newValue = onMove)
val currentOnOver = rememberUpdatedState(newValue = onOver) val currentIsMoveEnable = rememberUpdatedState(newValue = isMoveEnable)
val currentOnDrop = rememberUpdatedState(newValue = onDrop) val currentOnDragRelease = rememberUpdatedState(newValue = onDragRelease)
val currentOnDropReleaseOver = rememberUpdatedState(newValue = onDropReleaseOver)
val state = remember(gridState, scope, currentOnMove) { val state = remember(gridState, scope, currentOnMove) {
GridDragDropState( GridDragDropState(
@ -68,8 +70,9 @@ fun rememberGridDragDropState(
scope = scope, scope = scope,
state = gridState, state = gridState,
onMove = currentOnMove.value, onMove = currentOnMove.value,
onOver = currentOnOver.value, isMoveEnable = currentIsMoveEnable.value,
onDrop = currentOnDrop.value, onDragRelease = currentOnDragRelease.value,
onDropReleaseOver = currentOnDropReleaseOver.value,
) )
} }
@ -88,34 +91,38 @@ class GridDragDropState internal constructor(
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val state: LazyGridState, private val state: LazyGridState,
private val onMove: (Int, Int) -> Unit, private val onMove: (Int, Int) -> Unit,
private val onOver: (Int, Int) -> Boolean, private val isMoveEnable: (Int, Int) -> Boolean,
private val onDrop: (Int, Int) -> Unit, private val onDragRelease: (Int) -> Unit,
private val onDropReleaseOver: (Int, Int) -> Unit,
) { ) {
internal val scrollChannel = Channel<Float>() internal val scrollChannel = Channel<Float>()
var overItemIndex by mutableStateOf<Int?>(null)
private set
private var previousItemIndex by mutableStateOf<Int?>(null)
var draggingItemIndex by mutableStateOf<Int?>(null) var draggingItemIndex by mutableStateOf<Int?>(null)
private set private set
var overItemIndex by mutableStateOf<Int?>(null) internal var draggingItemLingeringIndex by mutableStateOf<Int?>(null)
private set private set
private var draggingItemInitialOffset by mutableStateOf(Offset.Zero) private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero) private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
private val draggingItemLayoutInfo: LazyGridItemInfo?
get() = state.layoutInfo.visibleItemsInfo
.firstOrNull { it.index == draggingItemIndex }
internal val draggingItemOffset: Offset internal val draggingItemOffset: Offset
get() = draggingItemLayoutInfo get() = draggingItemLayoutInfo
?.let { item -> draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset() } ?.let { item -> draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset() }
?: Offset.Zero ?: Offset.Zero
private val draggingItemLayoutInfo: LazyGridItemInfo? internal var draggingItemSetIntoPositionAnimation =
get() = state.layoutInfo.visibleItemsInfo Animatable(initialValue = Offset.Zero, typeConverter = Offset.VectorConverter)
.firstOrNull { it.index == draggingItemIndex }
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
private set
internal var previousItemOffset = Animatable(Offset.Zero, Offset.VectorConverter)
private set private set
internal fun onDragStart(offset: Offset) { internal fun onDragStart(offset: Offset) {
@ -127,33 +134,6 @@ class GridDragDropState internal constructor(
} }
} }
internal fun onDragInterrupted(offset: Offset) {
val localDraggingItemIndex = draggingItemIndex
if (localDraggingItemIndex != null) {
state.layoutInfo.visibleItemsInfo
.firstItemWithOffsetOrNull(offset)
?.let { onDrop(localDraggingItemIndex, it.index) }
previousIndexOfDraggedItem = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
previousItemOffset.snapTo(startOffset)
previousItemOffset.animateTo(
Offset.Zero,
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Offset.VisibilityThreshold
)
)
previousIndexOfDraggedItem = null
}
}
overItemIndex = null
draggingItemDraggedDelta = Offset.Zero
draggingItemIndex = null
draggingItemInitialOffset = Offset.Zero
}
internal fun onDrag(offset: Offset) { internal fun onDrag(offset: Offset) {
draggingItemDraggedDelta += offset draggingItemDraggedDelta += offset
@ -170,15 +150,17 @@ class GridDragDropState internal constructor(
item.offset.x <= middleXOffset && middleXOffset <= item.offsetEnd.x && item.offset.x <= middleXOffset && middleXOffset <= item.offsetEnd.x &&
item.offset.y <= middleYOffset && middleYOffset <= item.offsetEnd.y item.offset.y <= middleYOffset && middleYOffset <= item.offsetEnd.y
} }
overItemIndex = targetItem?.index
if (targetItem != null) { if (targetItem != null) {
if (onOver.invoke(draggingItem.index, targetItem.index)) { if (isMoveEnable.invoke(draggingItem.index, targetItem.index)) {
onMove.invoke(draggingItem.index, targetItem.index) onMove.invoke(draggingItem.index, targetItem.index)
previousItemIndex = draggingItemIndex
draggingItemIndex = targetItem.index draggingItemIndex = targetItem.index
} else {
previousItemIndex = null
overItemIndex = targetItem.index
} }
} else { } else {
overItemIndex = null
val overscroll = when { val overscroll = when {
draggingItemDraggedDelta.y > 0 -> draggingItemDraggedDelta.y > 0 ->
(endOffset.y - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) (endOffset.y - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
@ -194,6 +176,79 @@ class GridDragDropState internal constructor(
} }
} }
internal fun onDragInterrupted(offset: Offset) {
val localDraggingItemIndex = draggingItemIndex
if (localDraggingItemIndex != null) {
val target = state.layoutInfo.visibleItemsInfo.firstItemWithOffsetOrNull(offset)
when {
target == null && previousItemIndex != draggingItemIndex -> {
// release have occur on the current place of the item but outside of range
onDragRelease(localDraggingItemIndex)
// play a animation so the item fit in place.
draggingItemLingeringIndex = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
draggingItemSetIntoPositionAnimation.snapTo(targetValue = startOffset)
draggingItemSetIntoPositionAnimation.animateTo(
targetValue = Offset.Zero,
animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Offset.VisibilityThreshold
)
)
draggingItemLingeringIndex = null
}
}
target == null -> {
// release have occur outside valid range. play and animation the make the item go back in place.
draggingItemLingeringIndex = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
draggingItemSetIntoPositionAnimation.snapTo(targetValue = startOffset)
draggingItemSetIntoPositionAnimation.animateTo(
targetValue = Offset.Zero,
animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Offset.VisibilityThreshold
)
)
draggingItemLingeringIndex = null
}
}
target.index == localDraggingItemIndex -> {
// release have occur on the current place of the item
onDragRelease(localDraggingItemIndex)
// play a animation so the item fit in place.
draggingItemLingeringIndex = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
draggingItemSetIntoPositionAnimation.snapTo(targetValue = startOffset)
draggingItemSetIntoPositionAnimation.animateTo(
targetValue = Offset.Zero,
animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Offset.VisibilityThreshold
)
)
draggingItemLingeringIndex = null
}
}
target.index != localDraggingItemIndex -> {
onDropReleaseOver(localDraggingItemIndex, target.index)
}
}
}
previousItemIndex = null
overItemIndex = null
draggingItemDraggedDelta = Offset.Zero
draggingItemIndex = null
draggingItemInitialOffset = Offset.Zero
}
private fun List<LazyGridItemInfo>.firstItemWithOffsetOrNull( private fun List<LazyGridItemInfo>.firstItemWithOffsetOrNull(
offset: Offset, offset: Offset,
): LazyGridItemInfo? { ): LazyGridItemInfo? {
@ -265,12 +320,12 @@ fun LazyGridItemScope.DraggableItem(
} }
} }
index == dragDropState.previousIndexOfDraggedItem -> { index == dragDropState.draggingItemLingeringIndex -> {
Modifier Modifier
.zIndex(1f) .zIndex(1f)
.graphicsLayer { .graphicsLayer {
translationX = dragDropState.previousItemOffset.value.x translationX = dragDropState.draggingItemSetIntoPositionAnimation.value.x
translationY = dragDropState.previousItemOffset.value.y translationY = dragDropState.draggingItemSetIntoPositionAnimation.value.y
} }
} }

View file

@ -1,9 +1,26 @@
package com.pixelized.rplexicon.utilitary.extentions.string package com.pixelized.rplexicon.utilitary.extentions.string
import android.net.Uri
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
object BaldurGageImageCache { object BaldurGageImageCache {
// https://bg3.wiki/wiki/Category:Controller_UI_Icons
private val uri = mapOf(
// Default.
null to R.drawable.icbg_generic_darkness_icon,
// Category:Generic Controller Icons
"https://bg3.wiki/w/images/5/56/Generic_Darkness_Icon.webp" to R.drawable.icbg_generic_darkness_icon,
// Category:Container Controller Icons
"https://bg3.wiki/w/images/3/39/Backpack_A_Unfaded.webp" to R.drawable.icbg_backpack_a_unfaded,
"https://bg3.wiki/w/images/6/6d/Backpack_B_Unfaded.webp" to R.drawable.icbg_backpack_b_unfaded,
// Category:Potion Controller Icons
"https://bg3.wiki/w/images/c/ce/POT_Potion_of_Healing_Unfaded.png" to R.drawable.icbg_pot_potion_of_healing_unfaded,
"https://bg3.wiki/w/images/7/73/POT_Potion_of_Superior_Healing_Unfaded.png" to R.drawable.icbg_potion_of_superior_healing_unfaded,
// Category:Cloak Controller Icons
"https://bg3.wiki/w/images/8/85/Cloak_Of_Protection_Unfaded.png" to R.drawable.icbg_cloak_of_protection_unfaded,
)
private val alterations = hashMapOf( private val alterations = hashMapOf(
"Arme enflammée" to R.drawable.icbg_flaming_blade, "Arme enflammée" to R.drawable.icbg_flaming_blade,
"Critique" to R.drawable.icbg_frenzied_strike, "Critique" to R.drawable.icbg_frenzied_strike,
@ -268,4 +285,6 @@ object BaldurGageImageCache {
@DrawableRes @DrawableRes
fun spellIcon(name: String): Int? = spells[name] fun spellIcon(name: String): Int? = spells[name]
fun cache(url: Uri?): Any = uri[url.toString()] ?: uri[null]!!
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB