diff --git a/app/src/main/java/com/pixelized/rplexicon/LauncherViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/LauncherViewModel.kt index f324722..56b7f5c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/LauncherViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/LauncherViewModel.kt @@ -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)) diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/Inventory.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/Inventory.kt deleted file mode 100644 index 758bc70..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/data/model/Inventory.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.pixelized.rplexicon.data.model - -data class Inventory( - val items: List, -) { - class Builder( - val items: MutableList = 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, - ) { - class Builder( - var name: String, - var amount: String? = null, - val items: MutableList = 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 Iterable.localFind(predicate: (T) -> T?): T? { - var single: T? = null - for (element in this) { - single = predicate(element) - if (single != null) { - break - } - } - return single -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/item/Item.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/item/Item.kt new file mode 100644 index 0000000..d09580f --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/model/item/Item.kt @@ -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?, +) diff --git a/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterInventoryFire.kt b/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterInventoryFire.kt new file mode 100644 index 0000000..1c5b54c --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterInventoryFire.kt @@ -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> = emptyMap(), +) { + companion object { + const val INVENTORIES = "Inventories" + } +} diff --git a/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterSheetFire.kt b/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterSheetFire.kt index bcb5462..aa830f9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterSheetFire.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterSheetFire.kt @@ -25,7 +25,7 @@ data class CharacterSheetFire( @get:PropertyName(ALTERATIONS) @set:PropertyName(ALTERATIONS) - var alterations : Map = emptyMap() + var alterations: Map = emptyMap(), ) { @Keep @IgnoreExtraProperties diff --git a/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterSheetFireMap.kt b/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterSheetFireMap.kt index f743591..8120514 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterSheetFireMap.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/network/CharacterSheetFireMap.kt @@ -12,6 +12,6 @@ data class CharacterSheetFireMap( var characters: Map = emptyMap(), ) { companion object { - private const val CHARACTERS = "Characters" + const val CHARACTERS = "Characters" } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/network/ItemDto.kt b/app/src/main/java/com/pixelized/rplexicon/data/network/ItemDto.kt new file mode 100644 index 0000000..5eed274 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/network/ItemDto.kt @@ -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 = emptyList(), +) { + companion object { + private const val ID = "id" + private const val AMOUNT = "amount" + private const val CHILDREN = "children" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/InventoryParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/InventoryParser.kt deleted file mode 100644 index 14a4ed1..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/InventoryParser.kt +++ /dev/null @@ -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 = parserScope { - val inventories = hashMapOf() - - 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) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/ItemLexiconParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/ItemLexiconParser.kt new file mode 100644 index 0000000..e29d618 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/inventory/ItemLexiconParser.kt @@ -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 = parserScope { + val inventories = hashMapOf() + + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/Sheets.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/Sheets.kt index 8de6f82..d91b5b4 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/Sheets.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/Sheets.kt @@ -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" } diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/character/InventoryRepository.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/character/ItemsRepository.kt similarity index 62% rename from app/src/main/java/com/pixelized/rplexicon/data/repository/character/InventoryRepository.kt rename to app/src/main/java/com/pixelized/rplexicon/data/repository/character/ItemsRepository.kt index 7f252f4..b5097c9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/character/InventoryRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/character/ItemsRepository.kt @@ -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>(emptyMap()) - val data: StateFlow> get() = _data + private val _data = MutableStateFlow>(emptyMap()) + val data: StateFlow> 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() } diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/firebase/RealtimeDatabaseRepository.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/firebase/RealtimeDatabaseRepository.kt index 28a6761..712f3c4 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/firebase/RealtimeDatabaseRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/firebase/RealtimeDatabaseRepository.kt @@ -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>> = 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 = characterFireSheet.mapNotNull { it[character] } + fun getInventory(character: String): Flow> = + inventoryFire.map { it[character] ?: emptyList() } + fun getThrows(): Flow = networkThrows @@ -193,6 +229,13 @@ class RealtimeDatabaseRepository @Inject constructor( reference.setValue(value.value) } + fun setInventory(character: String?, inventory: List) { + 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt index 99f4b97..e11eabd 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetScreen.kt @@ -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) } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt index 9e459ae..78764c6 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/CharacterSheetViewModel.kt @@ -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)) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/InventoryItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/InventoryItem.kt deleted file mode 100644 index 7fd6f74..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/actions/InventoryItem.kt +++ /dev/null @@ -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 = 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, - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/preview/rememberInventoryListState.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/preview/rememberInventoryListState.kt index 74b0bfc..a16dcb4 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/preview/rememberInventoryListState.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/composable/preview/rememberInventoryListState.kt @@ -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> { 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"), ) ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/factory/ItemUioFactory.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/factory/ItemUioFactory.kt index 184af02..2e6374c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/factory/ItemUioFactory.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/factory/ItemUioFactory.kt @@ -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( - name = item.name, - amount = item.amount, - items = item.items.map { toUio(it) } - ) + fun toUio( + items: Map, + fires: List, + ): List { + 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) }, + ) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryItem.kt index 3829e92..234320f 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryItem.kt @@ -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 = emptyList(), + val icon: Any? = R.drawable.icbg_generic_darkness_icon, + val items: List? = 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 { override val values: Sequence = 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, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryPage.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryPage.kt index 715a87d..d033235 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryPage.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryPage.kt @@ -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(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,37 +83,102 @@ fun InventoryPage( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun InventoryPageContent( modifier: Modifier = Modifier, equipments: State, inventory: State>, 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, - ) - } + EquipmentItem( + modifier = Modifier.padding(bottom = 16.dp), + equipments = it, + onClick = onEquipment, + ) } - if (inventory.value.isNotEmpty()) { - stickyHeader { - GenericHeader( - label = R.string.character_sheet_title_inventory - ) - } - items(items = inventory.value) { - InventoryItem( - item = it, - ) + 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 + } + } + 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(), inventory = rememberInventoryListState(), onEquipment = { }, + onInventoryItemMove = { _, _ -> }, + onInventoryIsMoveEnable = { _, _ -> true }, + onInventoryItemDrop = { _ -> }, + onInventoryItemDropOver = { _, _ -> } ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryViewModel.kt index f822a4f..d423242 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/InventoryViewModel.kt @@ -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(null) val equipments: State get() = _equipments - private val _inventory = mutableStateOf>(emptyList()) - val inventory: State> get() = _inventory + // Target the local indexes form specific ids (change locally to avoid flooding of firebase) + private val itemLocalIndex = + MutableStateFlow>(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> + @Composable + @Stable + get() { + return _inventory.collectAsState(emptyList()) + } private val _dialog = mutableStateOf(null) val dialog: State 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 + lateinit var fire: List + lateinit var indexes: Map + operator fun component1() = items + operator fun component2() = fire + operator fun component3() = indexes + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/Pouet.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/Pouet.kt deleted file mode 100644 index bcf8e51..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/character/pages/inventory/Pouet.kt +++ /dev/null @@ -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, - ) - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/LazyGrid+DragAndDrop.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/LazyGrid+DragAndDrop.kt index 8eccf52..ed973c4 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/LazyGrid+DragAndDrop.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/LazyGrid+DragAndDrop.kt @@ -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() + var overItemIndex by mutableStateOf(null) + private set + + private var previousItemIndex by mutableStateOf(null) + var draggingItemIndex by mutableStateOf(null) private set - var overItemIndex by mutableStateOf(null) + internal var draggingItemLingeringIndex by mutableStateOf(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(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.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 } } diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/string/BaldurGageImageCache.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/string/BaldurGageImageCache.kt index 136fcf9..70f645c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/string/BaldurGageImageCache.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/string/BaldurGageImageCache.kt @@ -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]!! } \ No newline at end of file diff --git a/app/src/main/res/drawable/icbg_backpack_a_unfaded.webp b/app/src/main/res/drawable/icbg_backpack_a_unfaded.webp new file mode 100644 index 0000000..32a3d29 Binary files /dev/null and b/app/src/main/res/drawable/icbg_backpack_a_unfaded.webp differ diff --git a/app/src/main/res/drawable/icbg_backpack_b_unfaded.webp b/app/src/main/res/drawable/icbg_backpack_b_unfaded.webp new file mode 100644 index 0000000..c977a73 Binary files /dev/null and b/app/src/main/res/drawable/icbg_backpack_b_unfaded.webp differ diff --git a/app/src/main/res/drawable/icbg_generic_darkness_icon.webp b/app/src/main/res/drawable/icbg_generic_darkness_icon.webp new file mode 100644 index 0000000..7cfe341 Binary files /dev/null and b/app/src/main/res/drawable/icbg_generic_darkness_icon.webp differ