Naive implementation to link inventory to firebase.
This commit is contained in:
parent
531e4bea98
commit
464aea6fd6
26 changed files with 766 additions and 690 deletions
|
|
@ -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.DescriptionRepository
|
||||
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.SkillRepository
|
||||
import com.pixelized.rplexicon.data.repository.character.SpellRepository
|
||||
|
|
@ -45,7 +45,7 @@ class LauncherViewModel @Inject constructor(
|
|||
spellRepository: SpellRepository,
|
||||
skillRepository: SkillRepository,
|
||||
descriptionRepository: DescriptionRepository,
|
||||
inventoryRepository: InventoryRepository,
|
||||
itemsRepository: ItemsRepository,
|
||||
equipmentRepository: EquipmentRepository,
|
||||
removeConRepository: RemoteConfigRepository // Unused but injected to initialize it.
|
||||
) : ViewModel() {
|
||||
|
|
@ -108,7 +108,7 @@ class LauncherViewModel @Inject constructor(
|
|||
}
|
||||
val inventory = async {
|
||||
try {
|
||||
inventoryRepository.fetchInventory()
|
||||
itemsRepository.fetchItems()
|
||||
} catch (exception: Exception) {
|
||||
Log.e(TAG, exception.message, exception)
|
||||
_error.emit(FetchErrorUio.Structure(type = Type.INVENTORY))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ data class CharacterSheetFire(
|
|||
|
||||
@get:PropertyName(ALTERATIONS)
|
||||
@set:PropertyName(ALTERATIONS)
|
||||
var alterations : Map<String, Boolean> = emptyMap()
|
||||
var alterations: Map<String, Boolean> = emptyMap(),
|
||||
) {
|
||||
@Keep
|
||||
@IgnoreExtraProperties
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ data class CharacterSheetFireMap(
|
|||
var characters: Map<String, CharacterSheetFire> = emptyMap(),
|
||||
) {
|
||||
companion object {
|
||||
private const val CHARACTERS = "Characters"
|
||||
const val CHARACTERS = "Characters"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,9 +19,9 @@ object CharacterBinder {
|
|||
const val MAGIC = "Magies"
|
||||
const val SKILL = "Capacités"
|
||||
const val MAGIC_LEXICON = "Lexique magique"
|
||||
const val ITEMS_LEXICON = "Lexique des objets"
|
||||
const val ALTERATION = "Altérations"
|
||||
const val DESCRIPTION = "Descriptions"
|
||||
const val INVENTORY = "Inventaires"
|
||||
const val EQUIPMENT = "Équipements"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.pixelized.rplexicon.data.repository.character
|
||||
|
||||
import com.pixelized.rplexicon.data.model.Inventory
|
||||
import com.pixelized.rplexicon.data.parser.inventory.InventoryParser
|
||||
import com.pixelized.rplexicon.data.model.item.Item
|
||||
import com.pixelized.rplexicon.data.parser.inventory.ItemLexiconParser
|
||||
import com.pixelized.rplexicon.data.repository.CharacterBinder
|
||||
import com.pixelized.rplexicon.data.repository.GoogleSheetServiceRepository
|
||||
import com.pixelized.rplexicon.utilitary.Update
|
||||
|
|
@ -12,23 +12,21 @@ import javax.inject.Inject
|
|||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class InventoryRepository @Inject constructor(
|
||||
class ItemsRepository @Inject constructor(
|
||||
private val googleRepository: GoogleSheetServiceRepository,
|
||||
private val inventoryParser: InventoryParser,
|
||||
private val itemLexiconParser: ItemLexiconParser,
|
||||
) {
|
||||
private val _data = MutableStateFlow<Map<String, Inventory>>(emptyMap())
|
||||
val data: StateFlow<Map<String, Inventory>> get() = _data
|
||||
private val _data = MutableStateFlow<Map<String, Item>>(emptyMap())
|
||||
val data: StateFlow<Map<String, Item>> get() = _data
|
||||
|
||||
var lastSuccessFullUpdate: Update = Update.INITIAL
|
||||
private set
|
||||
|
||||
fun find(name: String?): Inventory? = _data.value[name]
|
||||
|
||||
@Throws(IncompatibleSheetStructure::class, Exception::class)
|
||||
suspend fun fetchInventory() {
|
||||
suspend fun fetchItems() {
|
||||
googleRepository.fetch { sheet ->
|
||||
val request = sheet.get(CharacterBinder.ID, CharacterBinder.INVENTORY)
|
||||
val data = inventoryParser.parse(sheet = request.execute())
|
||||
val request = sheet.get(CharacterBinder.ID, CharacterBinder.ITEMS_LEXICON)
|
||||
val data = itemLexiconParser.parse(sheet = request.execute())
|
||||
_data.tryEmit(data)
|
||||
lastSuccessFullUpdate = Update.currentTime()
|
||||
}
|
||||
|
|
@ -9,11 +9,12 @@ import com.google.firebase.database.ktx.database
|
|||
import com.google.firebase.ktx.Firebase
|
||||
import com.pixelized.rplexicon.R
|
||||
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.CharacterSheetFireMap
|
||||
import com.pixelized.rplexicon.data.network.ItemDto
|
||||
import com.pixelized.rplexicon.data.network.NetworkThrow
|
||||
import com.pixelized.rplexicon.data.network.NetworkThrowMap
|
||||
import com.pixelized.rplexicon.data.network.NetworkThrowMap.Companion.CHARACTERS_THROWS
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
|
@ -74,6 +75,38 @@ class RealtimeDatabaseRepository @Inject constructor(
|
|||
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 {
|
||||
// reference to the node
|
||||
val reference = database.getReference("/")
|
||||
|
|
@ -133,6 +166,9 @@ class RealtimeDatabaseRepository @Inject constructor(
|
|||
fun getCharacter(character: String): Flow<CharacterSheetFire> =
|
||||
characterFireSheet.mapNotNull { it[character] }
|
||||
|
||||
fun getInventory(character: String): Flow<List<ItemDto>> =
|
||||
inventoryFire.map { it[character] ?: emptyList() }
|
||||
|
||||
fun getThrows(): Flow<NetworkThrowMap> =
|
||||
networkThrows
|
||||
|
||||
|
|
@ -193,6 +229,13 @@ class RealtimeDatabaseRepository @Inject constructor(
|
|||
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) {
|
||||
character?.let {
|
||||
val reference = database.getReference("$PATH_THROWS/")
|
||||
|
|
@ -202,7 +245,8 @@ class RealtimeDatabaseRepository @Inject constructor(
|
|||
|
||||
companion object {
|
||||
private const val TAG = "FirebaseRepository"
|
||||
private const val PATH_CHARACTERS = "Characters"
|
||||
private const val PATH_THROWS = CHARACTERS_THROWS
|
||||
private const val PATH_CHARACTERS = CharacterSheetFireMap.CHARACTERS
|
||||
private const val PATH_THROWS = NetworkThrowMap.CHARACTERS_THROWS
|
||||
private const val PATH_INVENTORY = CharacterInventoryFire.INVENTORIES
|
||||
}
|
||||
}
|
||||
|
|
@ -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.CharacterSheetHeaderUio
|
||||
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.pages.actions.ActionPage
|
||||
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.AlterationPagePreview
|
||||
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.InventoryPagePreview
|
||||
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryViewModel
|
||||
|
|
@ -208,7 +208,12 @@ fun CharacterSheetScreen(
|
|||
onLevel = { spell, level ->
|
||||
scope.launch {
|
||||
sheetState.hide()
|
||||
overlay.prepareRoll(diceThrow = spellsViewModel.onCastSpell(spell, level))
|
||||
overlay.prepareRoll(
|
||||
diceThrow = spellsViewModel.onCastSpell(
|
||||
spell,
|
||||
level
|
||||
)
|
||||
)
|
||||
overlay.showOverlay()
|
||||
}
|
||||
},
|
||||
|
|
@ -433,12 +438,7 @@ private fun rememberHeaderTabsState(
|
|||
else -> emptyList()
|
||||
}
|
||||
)
|
||||
addAll(
|
||||
when {
|
||||
inventoryViewModel.inventory.value.isNotEmpty() -> listOf(Inventory)
|
||||
else -> emptyList()
|
||||
}
|
||||
)
|
||||
add(Inventory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.DescriptionRepository
|
||||
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.SkillRepository
|
||||
import com.pixelized.rplexicon.data.repository.character.SpellRepository
|
||||
|
|
@ -33,7 +33,7 @@ class CharacterSheetViewModel @Inject constructor(
|
|||
private val characterRepository: CharacterSheetRepository,
|
||||
private val descriptionRepository: DescriptionRepository,
|
||||
private val alterationRepository: AlterationRepository,
|
||||
private val inventoryRepository: InventoryRepository,
|
||||
private val itemsRepository: ItemsRepository,
|
||||
private val equipmentRepository: EquipmentRepository,
|
||||
private val actionRepository: ActionRepository,
|
||||
private val objectRepository: ObjectActionRepository,
|
||||
|
|
@ -86,9 +86,9 @@ class CharacterSheetViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
val inventory = async {
|
||||
if (force || inventoryRepository.lastSuccessFullUpdate.shouldUpdate()) {
|
||||
if (force || itemsRepository.lastSuccessFullUpdate.shouldUpdate()) {
|
||||
try {
|
||||
inventoryRepository.fetchInventory()
|
||||
itemsRepository.fetchItems()
|
||||
} catch (exception: Exception) {
|
||||
Log.e(TAG, exception.message, exception)
|
||||
_error.emit(FetchErrorUio.Structure(type = Type.INVENTORY))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,37 +5,114 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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
|
||||
@Stable
|
||||
fun rememberInventoryListState(): State<List<InventoryItemUio>> {
|
||||
return remember {
|
||||
var id = 0
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
InventoryItemUio(
|
||||
name = "Bourse",
|
||||
items = listOf(
|
||||
InventoryItemUio(name = "Or", amount = "21"),
|
||||
),
|
||||
id = "${id++}",
|
||||
name = "Pouch",
|
||||
amount = 1,
|
||||
container = true,
|
||||
icon = R.drawable.icbg_pouch_a_unfaded,
|
||||
),
|
||||
InventoryItemUio(
|
||||
name = "Sac à dos",
|
||||
items = listOf(
|
||||
InventoryItemUio(name = "Sac de couchage"),
|
||||
InventoryItemUio(name = "Kit de cuisine"),
|
||||
InventoryItemUio(name = "Boite d'allume-feu"),
|
||||
InventoryItemUio(name = "Torches", amount = "10"),
|
||||
InventoryItemUio(name = "Rations journalières", amount = "10"),
|
||||
InventoryItemUio(name = "Outre d'eau"),
|
||||
InventoryItemUio(name = "Cordes", amount = "15 mètres"),
|
||||
InventoryItemUio(name = "Piège de chasse"),
|
||||
InventoryItemUio(name = "Bâton de marche"),
|
||||
id = "${id++}",
|
||||
name = "Backpack",
|
||||
amount = 1,
|
||||
container = true,
|
||||
icon = R.drawable.icbg_backpack_a_unfaded,
|
||||
),
|
||||
InventoryItemUio(
|
||||
id = "${id++}",
|
||||
name = "Scroll of blessing",
|
||||
amount = 1,
|
||||
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"),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,29 @@
|
|||
package com.pixelized.rplexicon.ui.screens.character.factory
|
||||
|
||||
import com.pixelized.rplexicon.data.model.Inventory
|
||||
import com.pixelized.rplexicon.ui.screens.character.composable.actions.InventoryItemUio
|
||||
import com.pixelized.rplexicon.data.model.item.Item
|
||||
import com.pixelized.rplexicon.data.network.ItemDto
|
||||
import com.pixelized.rplexicon.ui.screens.character.pages.inventory.InventoryItemUio
|
||||
import javax.inject.Inject
|
||||
|
||||
class ItemUioFactory @Inject constructor() {
|
||||
|
||||
fun toUio(item: Inventory.Item): InventoryItemUio {
|
||||
return InventoryItemUio(
|
||||
fun toUio(
|
||||
items: Map<String, Item>,
|
||||
fires: List<ItemDto>,
|
||||
): List<InventoryItemUio> {
|
||||
return fires.mapNotNull { fire ->
|
||||
items[fire.id]?.let { item ->
|
||||
fire.amount?.let { amount ->
|
||||
InventoryItemUio(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
amount = item.amount,
|
||||
items = item.items.map { toUio(it) }
|
||||
amount = amount,
|
||||
container = item.isContainer,
|
||||
icon = item.icon,
|
||||
items = null // fire.children?.let { toUio(items = items, fires = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,12 +34,12 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
|||
|
||||
@Stable
|
||||
data class InventoryItemUio(
|
||||
val id: Int,
|
||||
val id: String,
|
||||
val name: String,
|
||||
val amount: Int,
|
||||
val amount: Int = 1,
|
||||
val container: Boolean,
|
||||
val icon: Any?,
|
||||
val items: List<InventoryItemUio> = emptyList(),
|
||||
val icon: Any? = R.drawable.icbg_generic_darkness_icon,
|
||||
val items: List<InventoryItemUio>? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
@ -57,6 +57,39 @@ fun InventoryItem(
|
|||
model = item.icon,
|
||||
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(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.BottomEnd)
|
||||
|
|
@ -68,13 +101,11 @@ fun InventoryItem(
|
|||
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()
|
||||
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()
|
||||
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.
|
||||
|
|
@ -90,42 +121,7 @@ fun InventoryItem(
|
|||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.End,
|
||||
text = amount.takeIf { it > 0 }?.let { "$it" } ?: " ", //
|
||||
)
|
||||
}
|
||||
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" } ?: " ", //
|
||||
text = amount.takeIf { it > 1 }?.let { "$it" } ?: " ", //
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -150,23 +146,21 @@ private fun InventoryItemPreview(
|
|||
private class ClassInventoryItemProvider : PreviewParameterProvider<InventoryItemUio> {
|
||||
override val values: Sequence<InventoryItemUio> = sequenceOf(
|
||||
InventoryItemUio(
|
||||
id = 0,
|
||||
id = "0",
|
||||
name = "Pouch",
|
||||
amount = 1,
|
||||
container = true,
|
||||
icon = R.drawable.icbg_pouch_a_unfaded,
|
||||
|
||||
),
|
||||
InventoryItemUio(
|
||||
id = 1,
|
||||
id = "1",
|
||||
name = "Scroll of blessing",
|
||||
amount = 1,
|
||||
container = false,
|
||||
icon = R.drawable.icbg_scroll_of_bless_unfaded,
|
||||
|
||||
),
|
||||
InventoryItemUio(
|
||||
id = 2,
|
||||
id = "2",
|
||||
name = "Potion of blessing",
|
||||
amount = 2,
|
||||
container = false,
|
||||
|
|
|
|||
|
|
@ -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_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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
|
|
@ -16,19 +25,23 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.unit.dp
|
||||
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.EquipmentItemUio
|
||||
import com.pixelized.rplexicon.ui.screens.character.composable.actions.GenericHeader
|
||||
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.dialogs.SkillDetailDialog
|
||||
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.dialogs.SkillDetailDialog
|
||||
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.launch
|
||||
|
||||
|
|
@ -36,6 +49,7 @@ import kotlinx.coroutines.launch
|
|||
fun InventoryPage(
|
||||
viewModel: InventoryViewModel,
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val snack = LocalSnack.current
|
||||
val snackJob = remember { mutableStateOf<Job?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -48,6 +62,10 @@ fun InventoryPage(
|
|||
snackJob.value?.cancel()
|
||||
viewModel.showSkillDetailDialog(item = it)
|
||||
},
|
||||
onInventoryItemMove = viewModel::onMove,
|
||||
onInventoryIsMoveEnable = viewModel::isMoveEnable,
|
||||
onInventoryItemDrop = viewModel::onDrop,
|
||||
onInventoryItemDropOver = viewModel::onDropOver,
|
||||
)
|
||||
|
||||
SkillDetailDialog(
|
||||
|
|
@ -65,40 +83,105 @@ fun InventoryPage(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun InventoryPageContent(
|
||||
modifier: Modifier = Modifier,
|
||||
equipments: State<EquipmentItemUio?>,
|
||||
inventory: State<List<InventoryItemUio>>,
|
||||
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,
|
||||
contentPadding = PaddingValues(top = 8.dp, bottom = 16.dp),
|
||||
) {
|
||||
equipments.value?.let {
|
||||
item {
|
||||
EquipmentItem(
|
||||
modifier = Modifier.padding(bottom = 16.dp),
|
||||
equipments = it,
|
||||
onClick = onEquipment,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (inventory.value.isNotEmpty()) {
|
||||
stickyHeader {
|
||||
GenericHeader(
|
||||
label = R.string.character_sheet_title_inventory
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(5),
|
||||
modifier = Modifier
|
||||
.graphicsLayer { this.clip = false }
|
||||
.dragContainer(dragDropState)
|
||||
.fillMaxSize(),
|
||||
state = gridState,
|
||||
contentPadding = contentPadding,
|
||||
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
|
||||
}
|
||||
items(items = inventory.value) {
|
||||
}
|
||||
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 = it,
|
||||
item = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -112,6 +195,10 @@ fun InventoryPagePreview() {
|
|||
equipments = rememberEquipmentState(),
|
||||
inventory = rememberInventoryListState(),
|
||||
onEquipment = { },
|
||||
onInventoryItemMove = { _, _ -> },
|
||||
onInventoryIsMoveEnable = { _, _ -> true },
|
||||
onInventoryItemDrop = { _ -> },
|
||||
onInventoryItemDropOver = { _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
package com.pixelized.rplexicon.ui.screens.character.pages.inventory
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.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.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.factory.ItemUioFactory
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
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.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class InventoryViewModel @Inject constructor(
|
||||
private val inventoryRepository: InventoryRepository,
|
||||
private val equipmentRepository: EquipmentRepository,
|
||||
private val descriptionRepository: DescriptionRepository,
|
||||
private val fireRepository: RealtimeDatabaseRepository,
|
||||
private val itemFactory: ItemUioFactory,
|
||||
itemsRepository: ItemsRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
application: Application
|
||||
) : AndroidViewModel(application) {
|
||||
|
|
@ -40,8 +51,55 @@ class InventoryViewModel @Inject constructor(
|
|||
private val _equipments = mutableStateOf<EquipmentItemUio?>(null)
|
||||
val equipments: State<EquipmentItemUio?> get() = _equipments
|
||||
|
||||
private val _inventory = mutableStateOf<List<InventoryItemUio>>(emptyList())
|
||||
val inventory: State<List<InventoryItemUio>> get() = _inventory
|
||||
// Target the local indexes form specific ids (change locally to avoid flooding of firebase)
|
||||
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)
|
||||
val dialog: State<SkillDialogDetailUio?> get() = _dialog
|
||||
|
|
@ -52,11 +110,14 @@ class InventoryViewModel @Inject constructor(
|
|||
init {
|
||||
viewModelScope.launch {
|
||||
launch(Dispatchers.IO) {
|
||||
inventoryRepository.data.collect { inventories ->
|
||||
val items = inventories[character]?.items?.map { itemFactory.toUio(it) }
|
||||
withContext(Dispatchers.Main) {
|
||||
_inventory.value = items ?: emptyList()
|
||||
fireRepository.getInventory(character).collect { items ->
|
||||
itemLocalIndex.value = items.mapNotNull {
|
||||
if (it.id != null) {
|
||||
it.id as String to items.indexOf(it)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
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() {
|
||||
_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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,15 +47,17 @@ fun rememberGridDragDropState(
|
|||
contentPadding: PaddingValues,
|
||||
gridState: LazyGridState,
|
||||
onMove: (Int, Int) -> Unit,
|
||||
onOver: (Int, Int) -> Boolean,
|
||||
onDrop: (Int, Int) -> Unit,
|
||||
isMoveEnable: (Int, Int) -> Boolean,
|
||||
onDragRelease: (Int) -> Unit,
|
||||
onDropReleaseOver: (Int, Int) -> Unit,
|
||||
): GridDragDropState {
|
||||
val density = LocalDensity.current
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val currentOnMove = rememberUpdatedState(newValue = onMove)
|
||||
val currentOnOver = rememberUpdatedState(newValue = onOver)
|
||||
val currentOnDrop = rememberUpdatedState(newValue = onDrop)
|
||||
val currentIsMoveEnable = rememberUpdatedState(newValue = isMoveEnable)
|
||||
val currentOnDragRelease = rememberUpdatedState(newValue = onDragRelease)
|
||||
val currentOnDropReleaseOver = rememberUpdatedState(newValue = onDropReleaseOver)
|
||||
|
||||
val state = remember(gridState, scope, currentOnMove) {
|
||||
GridDragDropState(
|
||||
|
|
@ -68,8 +70,9 @@ fun rememberGridDragDropState(
|
|||
scope = scope,
|
||||
state = gridState,
|
||||
onMove = currentOnMove.value,
|
||||
onOver = currentOnOver.value,
|
||||
onDrop = currentOnDrop.value,
|
||||
isMoveEnable = currentIsMoveEnable.value,
|
||||
onDragRelease = currentOnDragRelease.value,
|
||||
onDropReleaseOver = currentOnDropReleaseOver.value,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -88,34 +91,38 @@ class GridDragDropState internal constructor(
|
|||
private val scope: CoroutineScope,
|
||||
private val state: LazyGridState,
|
||||
private val onMove: (Int, Int) -> Unit,
|
||||
private val onOver: (Int, Int) -> Boolean,
|
||||
private val onDrop: (Int, Int) -> Unit,
|
||||
private val isMoveEnable: (Int, Int) -> Boolean,
|
||||
private val onDragRelease: (Int) -> Unit,
|
||||
private val onDropReleaseOver: (Int, Int) -> Unit,
|
||||
) {
|
||||
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)
|
||||
private set
|
||||
|
||||
var overItemIndex by mutableStateOf<Int?>(null)
|
||||
internal var draggingItemLingeringIndex by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
|
||||
private var draggingItemInitialOffset 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
|
||||
get() = draggingItemLayoutInfo
|
||||
?.let { item -> draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset() }
|
||||
?: Offset.Zero
|
||||
|
||||
private val draggingItemLayoutInfo: LazyGridItemInfo?
|
||||
get() = state.layoutInfo.visibleItemsInfo
|
||||
.firstOrNull { it.index == draggingItemIndex }
|
||||
|
||||
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
|
||||
private set
|
||||
|
||||
internal var previousItemOffset = Animatable(Offset.Zero, Offset.VectorConverter)
|
||||
internal var draggingItemSetIntoPositionAnimation =
|
||||
Animatable(initialValue = Offset.Zero, typeConverter = Offset.VectorConverter)
|
||||
private set
|
||||
|
||||
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) {
|
||||
draggingItemDraggedDelta += offset
|
||||
|
||||
|
|
@ -170,15 +150,17 @@ class GridDragDropState internal constructor(
|
|||
item.offset.x <= middleXOffset && middleXOffset <= item.offsetEnd.x &&
|
||||
item.offset.y <= middleYOffset && middleYOffset <= item.offsetEnd.y
|
||||
}
|
||||
|
||||
overItemIndex = targetItem?.index
|
||||
|
||||
if (targetItem != null) {
|
||||
if (onOver.invoke(draggingItem.index, targetItem.index)) {
|
||||
if (isMoveEnable.invoke(draggingItem.index, targetItem.index)) {
|
||||
onMove.invoke(draggingItem.index, targetItem.index)
|
||||
previousItemIndex = draggingItemIndex
|
||||
draggingItemIndex = targetItem.index
|
||||
} else {
|
||||
previousItemIndex = null
|
||||
overItemIndex = targetItem.index
|
||||
}
|
||||
} else {
|
||||
overItemIndex = null
|
||||
val overscroll = when {
|
||||
draggingItemDraggedDelta.y > 0 ->
|
||||
(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(
|
||||
offset: Offset,
|
||||
): LazyGridItemInfo? {
|
||||
|
|
@ -265,12 +320,12 @@ fun LazyGridItemScope.DraggableItem(
|
|||
}
|
||||
}
|
||||
|
||||
index == dragDropState.previousIndexOfDraggedItem -> {
|
||||
index == dragDropState.draggingItemLingeringIndex -> {
|
||||
Modifier
|
||||
.zIndex(1f)
|
||||
.graphicsLayer {
|
||||
translationX = dragDropState.previousItemOffset.value.x
|
||||
translationY = dragDropState.previousItemOffset.value.y
|
||||
translationX = dragDropState.draggingItemSetIntoPositionAnimation.value.x
|
||||
translationY = dragDropState.draggingItemSetIntoPositionAnimation.value.y
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,26 @@
|
|||
package com.pixelized.rplexicon.utilitary.extentions.string
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.pixelized.rplexicon.R
|
||||
|
||||
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(
|
||||
"Arme enflammée" to R.drawable.icbg_flaming_blade,
|
||||
"Critique" to R.drawable.icbg_frenzied_strike,
|
||||
|
|
@ -268,4 +285,6 @@ object BaldurGageImageCache {
|
|||
|
||||
@DrawableRes
|
||||
fun spellIcon(name: String): Int? = spells[name]
|
||||
|
||||
fun cache(url: Uri?): Any = uri[url.toString()] ?: uri[null]!!
|
||||
}
|
||||
BIN
app/src/main/res/drawable/icbg_backpack_a_unfaded.webp
Normal file
BIN
app/src/main/res/drawable/icbg_backpack_a_unfaded.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/drawable/icbg_backpack_b_unfaded.webp
Normal file
BIN
app/src/main/res/drawable/icbg_backpack_b_unfaded.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
app/src/main/res/drawable/icbg_generic_darkness_icon.webp
Normal file
BIN
app/src/main/res/drawable/icbg_generic_darkness_icon.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Loading…
Add table
Add a link
Reference in a new issue