From 5bcb4367d6f71afea4c581f4b80ce36d0808844a Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Fri, 4 Apr 2025 17:05:29 +0200 Subject: [PATCH] Add item CRUD into the GameMaster screen --- .../composeResources/values/strings.xml | 13 +- .../desktop/lwa/DataSyncViewModel.kt | 8 +- .../com/pixelized/desktop/lwa/Module.kt | 14 +- .../desktop/lwa/network/LwaClient.kt | 23 +- .../desktop/lwa/network/LwaClientImpl.kt | 39 +- .../alteration/AlterationRepository.kt | 4 +- .../CharacterSheetRepository.kt | 3 +- .../characterSheet/CharacterSheetStore.kt | 4 +- .../lwa/repository/item/ItemRepository.kt | 39 ++ .../desktop/lwa/repository/item/ItemStore.kt | 138 ++++++ .../lwa/repository/tag/TagRepository.kt | 10 + .../desktop/lwa/repository/tag/TagStore.kt | 45 +- ...ectDestination.kt => GMItemDestination.kt} | 11 +- .../gamemaster/GMItemEditDestination.kt | 58 +++ .../edit/CharacterSheetEditViewModel.kt | 4 +- .../ui/screen/gamemaster/GameMasterNavHost.kt | 2 + .../alteration/edit/GMAlterationEditPage.kt | 10 +- .../edit/GMAlterationEditViewModel.kt | 2 +- .../alteration/list/GMAlteration.kt | 1 + .../alteration/list/GMAlterationPage.kt | 5 + .../alteration/list/GMAlterationViewModel.kt | 15 +- .../gamemaster/item/edit/GMItemEditFactory.kt | 123 ++++++ .../gamemaster/item/edit/GMItemEditPage.kt | 394 ++++++++++++++++++ .../item/edit/GMItemEditViewModel.kt | 77 ++++ .../ui/screen/gamemaster/item/list/GMItem.kt | 145 +++++++ .../gamemaster/item/list/GMItemFactory.kt | 51 +++ .../screen/gamemaster/item/list/GMItemPage.kt | 162 +++++++ .../gamemaster/item/list/GMItemViewModel.kt | 102 +++++ .../lwa/ui/screen/levelup/LevelUpViewModel.kt | 1 + .../pixelized/shared/lwa/model/item/Item.kt | 1 + .../shared/lwa/model/item/ItemJsonV1.kt | 1 + .../model/item/factory/ItemJsonFactoryV1.kt | 2 + 32 files changed, 1467 insertions(+), 40 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/item/ItemRepository.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/item/ItemStore.kt rename composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/{GMObjectDestination.kt => GMItemDestination.kt} (62%) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemEditDestination.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditViewModel.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItem.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemFactory.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemPage.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemViewModel.kt diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 0df87e7..3003768 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -232,6 +232,7 @@ Admin GameMaster + Sauvegarder Filtrer par nom : niv: %1$d Joueur @@ -253,7 +254,15 @@ Tags Identifiant du champ Expression - Sauvegarder - Annuler + Créer un objet + Supprimer un objet + Identifiant de l'altération + Nom + Description + Image url + Vignette url + Empilable + Équipable + Consommable \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt index 4cc3f87..ffd84cd 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository +import com.pixelized.desktop.lwa.repository.item.ItemRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.tag.TagRepository @@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.onEach class DataSyncViewModel( private val characterRepository: CharacterSheetRepository, private val alterationRepository: AlterationRepository, + private val itemRepository: ItemRepository, private val campaignRepository: CampaignRepository, private val tagRepository: TagRepository, private val settingsRepository: SettingsRepository, @@ -39,14 +41,18 @@ class DataSyncViewModel( @OptIn(ExperimentalCoroutinesApi::class) suspend fun synchronise() = coroutineScope { + networkRepository.data.onEach { println(it) }.launchIn(this) + networkRepository.status .filter { status -> status == NetworkRepository.Status.CONNECTED } .onEach { tagRepository.updateAlterationTags() - alterationRepository.updateAlterations() + alterationRepository.updateAlterationFlow() tagRepository.updateCharacterTags() characterRepository.updateCharacterPreviews() campaignRepository.updateCampaign() + tagRepository.updateItemTags() + itemRepository.updateItemFlow() } .launchIn(this) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 46b820b..57bff04 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -8,6 +8,8 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignStore import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetStore +import com.pixelized.desktop.lwa.repository.item.ItemRepository +import com.pixelized.desktop.lwa.repository.item.ItemStore import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.settings.SettingsFactory @@ -17,8 +19,8 @@ import com.pixelized.desktop.lwa.repository.tag.TagRepository import com.pixelized.desktop.lwa.repository.tag.TagStore import com.pixelized.desktop.lwa.ui.composable.character.alterteration.CharacterSheetAlterationDialogFactory import com.pixelized.desktop.lwa.ui.composable.character.alterteration.CharacterSheetAlterationDialogViewModel -import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory +import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogViewModel import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogFactory import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlayViewModel @@ -47,6 +49,10 @@ import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.list.GMAlterati import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list.GMCharacterFactory import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list.GMCharacterViewModel import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory +import com.pixelized.desktop.lwa.ui.screen.gamemaster.item.edit.GMItemEditFactory +import com.pixelized.desktop.lwa.ui.screen.gamemaster.item.edit.GMItemEditViewModel +import com.pixelized.desktop.lwa.ui.screen.gamemaster.item.list.GMItemFactory +import com.pixelized.desktop.lwa.ui.screen.gamemaster.item.list.GMItemViewModel import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpFactory import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel @@ -105,6 +111,7 @@ val storeDependencies singleOf(::AlterationStore) singleOf(::CampaignStore) singleOf(::TagStore) + singleOf(::ItemStore) } val repositoryDependencies @@ -116,6 +123,7 @@ val repositoryDependencies singleOf(::AlterationRepository) singleOf(::CampaignRepository) singleOf(::TagRepository) + singleOf(::ItemRepository) } val factoryDependencies @@ -137,6 +145,8 @@ val factoryDependencies factoryOf(::GMCharacterFactory) factoryOf(::GMAlterationFactory) factoryOf(::GMAlterationEditFactory) + factoryOf(::GMItemFactory) + factoryOf(::GMItemEditFactory) } val viewModelDependencies @@ -163,6 +173,8 @@ val viewModelDependencies viewModelOf(::GMActionViewModel) viewModelOf(::GMAlterationViewModel) viewModelOf(::GMAlterationEditViewModel) + viewModelOf(::GMItemViewModel) + viewModelOf(::GMItemEditViewModel) } val useCaseDependencies diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt index 80a3ad1..2e75a8a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt @@ -3,9 +3,10 @@ package com.pixelized.desktop.lwa.network import com.pixelized.shared.lwa.model.alteration.AlterationJson import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson +import com.pixelized.shared.lwa.model.item.ItemJson import com.pixelized.shared.lwa.model.tag.TagJson -import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson import com.pixelized.shared.lwa.protocol.rest.APIResponse +import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson interface LwaClient { @@ -56,6 +57,7 @@ interface LwaClient { suspend fun putCharacter( sheet: CharacterSheetJson, + create: Boolean, ): APIResponse suspend fun putCharacterDamage( @@ -83,12 +85,31 @@ interface LwaClient { characterSheetId: String, ): APIResponse + // Items + + suspend fun getItems(): APIResponse> + + suspend fun getItem( + itemId: String, + ): APIResponse + + suspend fun putItem( + itemJson: ItemJson, + create: Boolean, + ): APIResponse + + suspend fun deleteItem( + itemId: String, + ): APIResponse + // Tags suspend fun getAlterationTags(): APIResponse> suspend fun getCharacterTags(): APIResponse> + suspend fun getItemTags(): APIResponse> + companion object { fun error(error: APIResponse<*>): Nothing = throw LwaNetworkException(error) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index a24f2da..26769f8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt @@ -4,6 +4,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.shared.lwa.model.alteration.AlterationJson import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson +import com.pixelized.shared.lwa.model.item.ItemJson import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson @@ -98,8 +99,9 @@ class LwaClientImpl( @Throws override suspend fun putCharacter( sheet: CharacterSheetJson, + create: Boolean, ) = client - .put("$root/character/update/sheet") { + .put("$root/character/update/sheet?create=$create") { contentType(ContentType.Application.Json) setBody(sheet) } @@ -143,6 +145,36 @@ class LwaClientImpl( .delete("$root/character/delete?characterSheetId=$characterSheetId") .body>() + @Throws + override suspend fun getItems(): APIResponse> = client + .get("$root/item/all") + .body() + + @Throws + override suspend fun getItem( + itemId: String, + ): APIResponse = client + .get("$root/item/detail?itemId=$itemId") + .body() + + @Throws + override suspend fun putItem( + item: ItemJson, + create: Boolean, + ): APIResponse = client + .put("$root/item/update?create=$create") { + contentType(ContentType.Application.Json) + setBody(item) + } + .body>() + + @Throws + override suspend fun deleteItem( + itemId: String, + ): APIResponse = client + .delete("$root/item/delete?itemId=$itemId") + .body() + @Throws override suspend fun getAlterationTags(): APIResponse> = client .get("$root/tag/alteration") @@ -152,4 +184,9 @@ class LwaClientImpl( override suspend fun getCharacterTags(): APIResponse> = client .get("$root/tag/character") .body() + + @Throws + override suspend fun getItemTags(): APIResponse> = client + .get("$root/tag/item") + .body() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt index 18be883..78a1ae0 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.stateIn class AlterationRepository( private val alterationStore: AlterationStore, - campaignRepository: CampaignRepository, characterRepository: CharacterSheetRepository, ) { @@ -64,7 +63,7 @@ class AlterationRepository( initialValue = emptyMap(), ) - suspend fun updateAlterations() { + suspend fun updateAlterationFlow() { alterationStore.updateAlterationsFlow() } @@ -97,6 +96,7 @@ class AlterationRepository( ) } + @kotlin.jvm.Throws suspend fun deleteAlteration( alterationId: String, ) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt index a10d1f2..8ecb5ab 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt @@ -52,8 +52,9 @@ class CharacterSheetRepository( @Throws suspend fun updateCharacter( sheet: CharacterSheet, + create: Boolean, ) { - store.updateCharacterSheet(sheet = sheet) + store.updateCharacterSheet(sheet = sheet, create = create) } @Throws diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt index 86024b8..f842148 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt @@ -1,5 +1,6 @@ package com.pixelized.desktop.lwa.repository.characterSheet +import androidx.compose.runtime.clearCompositionErrors import com.pixelized.desktop.lwa.network.LwaClient import com.pixelized.desktop.lwa.repository.alteration.AlterationStore import com.pixelized.desktop.lwa.repository.network.NetworkRepository @@ -91,9 +92,10 @@ class CharacterSheetStore( @Throws suspend fun updateCharacterSheet( sheet: CharacterSheet, + create: Boolean, ) { val json = factory.convertToJson(sheet = sheet) - val request = client.putCharacter(sheet = json) + val request = client.putCharacter(sheet = json, create = create) if (request.success.not()) { LwaClient.error(error = request) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/item/ItemRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/item/ItemRepository.kt new file mode 100644 index 0000000..cf6f214 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/item/ItemRepository.kt @@ -0,0 +1,39 @@ +package com.pixelized.desktop.lwa.repository.item + +import com.pixelized.shared.lwa.model.item.Item + +class ItemRepository( + private val itemStore: ItemStore, +) { + val itemFlow get() = itemStore.items + + suspend fun updateItemFlow() { + itemStore.updateItemFlow() + } + + fun item( + itemId: String?, + ): Item? { + return itemFlow.value[itemId] + } + + @Throws + suspend fun updateItem( + item: Item, + create: Boolean, + ) { + itemStore.putItem( + item = item, + create = create, + ) + } + + @Throws + suspend fun deleteItem( + itemId: String, + ) { + itemStore.deleteItem( + itemId = itemId + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/item/ItemStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/item/ItemStore.kt new file mode 100644 index 0000000..234acee --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/item/ItemStore.kt @@ -0,0 +1,138 @@ +package com.pixelized.desktop.lwa.repository.item + +import com.pixelized.desktop.lwa.network.LwaClient +import com.pixelized.desktop.lwa.repository.network.NetworkRepository +import com.pixelized.shared.lwa.model.item.Item +import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactory +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import com.pixelized.shared.lwa.protocol.websocket.SocketMessage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class ItemStore( + private val network: NetworkRepository, + private val factory: ItemJsonFactory, + private val client: LwaClient, +) { + private val _items = MutableStateFlow>(emptyMap()) + val items: StateFlow> get() = _items + + init { + val scope = CoroutineScope(Dispatchers.IO + Job()) + // data update through WebSocket. + scope.launch { + network.data.collect(::handleMessage) + } + } + + fun items(): Collection { + return items.value.values + } + + fun item(itemId: String): Item? { + return items.value[itemId] + } + + suspend fun updateItemFlow() { + _items.value = try { + getItem() + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + emptyMap() + } + } + + private suspend fun updateItemFlow( + itemId: String, + ) { + val item = try { + getItem(itemId = itemId) + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + null + } + // guard case if getItem failed + if (item == null) return + // update the flow with the new item. + _items.update { items -> + items.toMutableMap().also { + it[itemId] = item + } + } + } + + @Throws + private suspend fun getItem(): Map { + val request = client.getItems() + return when (request.success) { + true -> request.data + ?.map { factory.convertFromJson(json = it) } + ?.associateBy { it.id } + ?: emptyMap() + + else -> LwaClient.error(error = request) + } + } + + @Throws + private suspend fun getItem( + itemId: String, + ): Item? { + val request = client.getItem(itemId = itemId) + return when (request.success) { + true -> request.data?.let { factory.convertFromJson(json = it) } + else -> LwaClient.error(error = request) + } + } + + @Throws + suspend fun putItem( + item: Item, + create: Boolean, + ) { + val request = client.putItem( + itemJson = factory.convertToJson(item = item), + create = create, + ) + if (request.success.not()) { + LwaClient.error(error = request) + } + } + + @Throws + suspend fun deleteItem( + itemId: String, + ) { + val request = client.deleteItem(itemId = itemId) + if (request.success.not()) { + LwaClient.error(error = request) + } + } + + // region: WebSocket & data update. + + private suspend fun handleMessage(message: SocketMessage) { + when (message) { + is ApiSynchronisation.ItemApiSynchronisation -> when (message) { + is ApiSynchronisation.ItemUpdate -> updateItemFlow( + itemId = message.itemId, + ) + + is ApiSynchronisation.ItemDelete -> _items.update { items -> + items.toMutableMap().also { + it.remove(message.itemId) + } + } + } + + else -> Unit + } + } + + // endregion +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagRepository.kt index b44049c..63a5f7e 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagRepository.kt @@ -10,7 +10,17 @@ class TagRepository( suspend fun updateAlterationTags() = store.updateAlterationTagsFlow() + suspend fun updateItemTags() = store.updateItemTagsFlow() + fun charactersTagFlow(): StateFlow> = store.charactersTagFlow() + fun charactersTags(): Collection = charactersTagFlow().value.values + fun alterationsTagFlow(): StateFlow> = store.alterationsTagFlow() + + fun alterationsTags(): Collection = alterationsTagFlow().value.values + + fun itemsTagFlow(): StateFlow> = store.itemsTagFlow() + + fun itemsTags(): Collection = itemsTagFlow().value.values } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagStore.kt index 3263260..0cb0ca3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagStore.kt @@ -2,7 +2,9 @@ package com.pixelized.desktop.lwa.repository.tag import com.pixelized.desktop.lwa.network.LwaClient import com.pixelized.shared.lwa.model.tag.Tag +import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.model.tag.TagJsonFactory +import com.pixelized.shared.lwa.protocol.rest.APIResponse import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -12,6 +14,7 @@ class TagStore( ) { private val characterTagsFlow = MutableStateFlow>(emptyMap()) private val alterationTagsFlow = MutableStateFlow>(emptyMap()) + private val itemTagsFlow = MutableStateFlow>(emptyMap()) fun charactersTagFlow(): StateFlow> = characterTagsFlow @@ -33,9 +36,19 @@ class TagStore( return alterationTagsFlow.value[tagId] } + fun itemsTagFlow(): StateFlow> = itemTagsFlow + + fun items(): Collection { + return itemTagsFlow.value.values + } + + fun item(tagId: String): Tag? { + return itemTagsFlow.value[tagId] + } + suspend fun updateCharacterTagsFlow() { characterTagsFlow.value = try { - getCharacterTag() + requestTag { client.getCharacterTags() } } catch (exception: Exception) { println(exception.message) // TODO proper exception handling emptyMap() @@ -44,7 +57,16 @@ class TagStore( suspend fun updateAlterationTagsFlow() { alterationTagsFlow.value = try { - getAlterationTag() + requestTag { client.getAlterationTags() } + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + emptyMap() + } + } + + suspend fun updateItemTagsFlow() { + itemTagsFlow.value = try { + requestTag { client.getItemTags() } } catch (exception: Exception) { println(exception.message) // TODO proper exception handling emptyMap() @@ -52,21 +74,10 @@ class TagStore( } @Throws - private suspend fun getCharacterTag(): Map { - val request = client.getCharacterTags() - return when (request.success) { - true -> request.data - ?.map { factory.convertFromJson(json = it) } - ?.associateBy { it.id } - ?: emptyMap() - - else -> LwaClient.error(error = request) - } - } - - @Throws - private suspend fun getAlterationTag(): Map { - val request = client.getAlterationTags() + private suspend inline fun requestTag( + crossinline block: suspend () -> APIResponse>, + ): Map { + val request = block() return when (request.success) { true -> request.data ?.map { factory.convertFromJson(json = it) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMObjectDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemDestination.kt similarity index 62% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMObjectDestination.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemDestination.kt index 43d05b1..a5246ab 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMObjectDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemDestination.kt @@ -3,9 +3,10 @@ package com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable +import com.pixelized.desktop.lwa.ui.screen.gamemaster.item.list.GMItemPage -object GMObjectDestination { - private const val ROUTE = "GameMasterObject" +object GMItemDestination { + private const val ROUTE = "GameMasterItem" fun baseRoute() = ROUTE fun navigationRoute() = ROUTE @@ -13,13 +14,13 @@ object GMObjectDestination { fun NavGraphBuilder.composableGameMasterObjectPage() { composable( - route = GMObjectDestination.baseRoute(), + route = GMItemDestination.baseRoute(), ) { - + GMItemPage() } } fun NavHostController.navigateToGameMasterObjectPage() { - val route = GMObjectDestination.navigationRoute() + val route = GMItemDestination.navigationRoute() navigate(route = route) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemEditDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemEditDestination.kt new file mode 100644 index 0000000..d8e0943 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemEditDestination.kt @@ -0,0 +1,58 @@ +package com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster + +import androidx.compose.runtime.Stable +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.pixelized.desktop.lwa.ui.screen.gamemaster.item.edit.GMItemEditPage +import com.pixelized.desktop.lwa.utils.extention.ARG + +@Stable +object GMItemEditDestination { + private const val ROUTE = "GameMasterItem" + private const val ITEM_ID = "id" + + @Stable + fun baseRoute() = "$ROUTE?${ITEM_ID.ARG}" + + @Stable + fun navigationRoute(itemId: String?) = "$ROUTE?$ITEM_ID=$itemId" + + @Stable + fun arguments() = listOf( + navArgument(ITEM_ID) { + nullable = true + type = NavType.StringType + }, + ) + + @Stable + data class Argument( + val id: String?, + ) { + constructor(savedStateHandle: SavedStateHandle) : this( + id = savedStateHandle.get(ITEM_ID), + ) + } +} + +fun NavGraphBuilder.composableGameMasterItemEditPage() { + composable( + route = GMItemEditDestination.baseRoute(), + arguments = GMItemEditDestination.arguments(), + ) { + GMItemEditPage() + } +} + +fun NavHostController.navigateToGameMasterItemEditPage( + itemId: String?, +) { + val route = GMItemEditDestination.navigationRoute( + itemId = itemId, + ) + navigate(route = route) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditViewModel.kt index 7150070..7dd43f6 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/characterSheet/edit/CharacterSheetEditViewModel.kt @@ -15,8 +15,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__critical_action_label -import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__description_label import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__default_action_label +import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__description_label import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__name_label import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__spacial_action_label import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__copy__label @@ -182,6 +182,7 @@ class CharacterSheetEditViewModel( ) characterSheetRepository.updateCharacter( sheet = updatedSheet, + create = argument.id == null, ) } @@ -194,6 +195,7 @@ class CharacterSheetEditViewModel( ) characterSheetRepository.updateCharacter( sheet = updatedSheet.copy(id = characterSheetId), + create = true, ) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterNavHost.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterNavHost.kt index 8b8b056..271be5b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterNavHost.kt @@ -12,6 +12,7 @@ import androidx.navigation.compose.rememberNavController import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GameMasterDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterAlterationEditPage +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterItemEditPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterMainPage val LocalGMScreenController = compositionLocalOf { @@ -36,6 +37,7 @@ fun GameMasterNavHost() { ) { composableGameMasterMainPage() composableGameMasterAlterationEditPage() + composableGameMasterItemEditPage() } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditPage.kt index d6a6da9..153775b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditPage.kt @@ -68,8 +68,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__action__save import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_add_field -import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_save import lwacharactersheet.composeapp.generated.resources.game_master__alteration__title import lwacharactersheet.composeapp.generated.resources.ic_save_24dp import org.jetbrains.compose.resources.painterResource @@ -103,12 +103,12 @@ fun GMAlterationEditScreen( ) { val screen = LocalScreenController.current val scope = rememberCoroutineScope() + val form = viewModel.form.collectAsState() GMAlterationEditContent( modifier = Modifier.fillMaxSize(), form = form, - paddings = GMAlterationEditPageDefault.paddings, onBack = { screen.navigateBack() }, @@ -147,7 +147,7 @@ private fun GMAlterationEditContent( scope: CoroutineScope = rememberCoroutineScope(), tagsState: LazyListState = rememberLazyListState(), form: State, - paddings: PaddingValues, + paddings: PaddingValues = GMAlterationEditPageDefault.paddings, onBack: () -> Unit, addField: () -> Unit, removeField: (index: Int) -> Unit, @@ -181,7 +181,7 @@ private fun GMAlterationEditContent( modifier = Modifier.padding(end = 4.dp), color = MaterialTheme.lwa.colorScheme.base.primary, fontWeight = FontWeight.SemiBold, - text = stringResource(Res.string.game_master__alteration__edit_field_save), + text = stringResource(Res.string.game_master__action__save), ) Icon( painter = painterResource(Res.drawable.ic_save_24dp), @@ -342,7 +342,7 @@ private fun GMAlterationEditContent( ) { Text( modifier = Modifier.padding(end = 4.dp), - text = stringResource(Res.string.game_master__alteration__edit_field_save), + text = stringResource(Res.string.game_master__action__save), ) Icon( painter = painterResource(Res.drawable.ic_save_24dp), diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditViewModel.kt index 216f220..bf7eb44 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditViewModel.kt @@ -36,7 +36,7 @@ class GMAlterationEditViewModel( _form.value = factory.createForm( originId = argument.id, alteration = alterationRepository.alteration(alterationId = argument.id), - tags = tagRepository.alterationsTagFlow().value.values, + tags = tagRepository.alterationsTags(), ) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlteration.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlteration.kt index 805c5d7..2385913 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlteration.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlteration.kt @@ -44,6 +44,7 @@ data class GMAlterationUio( @Stable object GMAlterationDefault { + @Stable val padding = PaddingValues(start = 16.dp) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationPage.kt index c03eb48..7374bc8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationPage.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterAlterationEditPage @@ -68,6 +69,10 @@ fun GMAlterationPage( screen.navigateToGameMasterAlterationEditPage(alterationId = null) }, ) + + ErrorSnackHandler( + error = viewModel.error, + ) } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationViewModel.kt index 2bb1646..27b0b79 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationViewModel.kt @@ -4,11 +4,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.tag.TagRepository +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio import com.pixelized.desktop.lwa.utils.extention.unAccent +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -30,6 +33,9 @@ class GMAlterationViewModel( private val selectedTagId = MutableStateFlow(null) private val filterValue = MutableStateFlow("") + private val _error = MutableSharedFlow() + val error: SharedFlow get() = _error + val filter = LwaTextFieldUio( enable = true, label = runBlocking { getString(Res.string.game_master__character__filter) }, @@ -84,6 +90,13 @@ class GMAlterationViewModel( } suspend fun deleteAlteration(alterationId: String) { - alterationRepository.deleteAlteration(alterationId) + try { + alterationRepository.deleteAlteration( + alterationId = alterationId, + ) + } catch (exception: Exception) { + val message = ErrorSnackUio.from(exception = exception) + _error.emit(message) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt new file mode 100644 index 0000000..61ab2a3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditFactory.kt @@ -0,0 +1,123 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.item.edit + +import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBoxUio +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory +import com.pixelized.shared.lwa.model.item.Item +import com.pixelized.shared.lwa.model.tag.Tag +import kotlinx.coroutines.flow.MutableStateFlow +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_description +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_id +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_image +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_label +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_thumbnail +import org.jetbrains.compose.resources.getString + +class GMItemEditFactory( + private val tagFactory: GMTagFactory, +) { + + suspend fun createForm( + originId: String?, + item: Item?, + tags: Collection, + ): GMItemEditPageUio { + val idFlow = MutableStateFlow(item?.id ?: "") + val labelFlow = MutableStateFlow(item?.metadata?.name ?: "") + val descriptionFlow = MutableStateFlow(item?.metadata?.description ?: "") + val imageFlow = MutableStateFlow(item?.metadata?.image ?: "") + val thumbnailFlow = MutableStateFlow(item?.metadata?.thumbnail ?: "") + val stackableFlow = MutableStateFlow(item?.options?.stackable ?: false) + val equipableFlow = MutableStateFlow(item?.options?.equipable ?: false) + val consumableFlow = MutableStateFlow(item?.options?.consumable ?: false) + + val tagFlow = MutableStateFlow( + tagFactory.convertToGMTagItemUio( + tags = tags, + selectedTagIds = item?.tags ?: emptyList(), + ) + ) + + return GMItemEditPageUio( + id = LwaTextFieldUio( + enable = originId == null, + isError = MutableStateFlow(false), + label = getString(Res.string.game_master__item__edit_id), + valueFlow = idFlow, + placeHolder = null, + onValueChange = { idFlow.value = it }, + ), + label = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + label = getString(Res.string.game_master__item__edit_label), + valueFlow = labelFlow, + placeHolder = null, + onValueChange = { labelFlow.value = it }, + ), + description = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + label = getString(Res.string.game_master__item__edit_description), + valueFlow = descriptionFlow, + placeHolder = null, + onValueChange = { descriptionFlow.value = it }, + ), + image = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + label = getString(Res.string.game_master__item__edit_image), + valueFlow = imageFlow, + placeHolder = null, + onValueChange = { descriptionFlow.value = it }, + ), + thumbnail = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + label = getString(Res.string.game_master__item__edit_thumbnail), + valueFlow = thumbnailFlow, + placeHolder = null, + onValueChange = { descriptionFlow.value = it }, + ), + equipable = LwaCheckBoxUio( + checked = equipableFlow, + onCheckedChange = { equipableFlow.value = it }, + ), + stackable = LwaCheckBoxUio( + checked = stackableFlow, + onCheckedChange = { stackableFlow.value = it }, + ), + consumable = LwaCheckBoxUio( + checked = consumableFlow, + onCheckedChange = { consumableFlow.value = it }, + ), + tags = tagFlow, + ) + } + + suspend fun createItem( + form: GMItemEditPageUio?, + ): Item? { + if (form == null) return null + + return Item( + id = form.id.valueFlow.value, + metadata = Item.MetaData( + name = form.label.valueFlow.value, + description = form.description.valueFlow.value, + image = form.image.valueFlow.value, + thumbnail = form.thumbnail.valueFlow.value, + ), + options = Item.Options( + stackable = form.stackable.checked.value, + equipable = form.equipable.checked.value, + consumable = form.consumable.checked.value, + ), + tags = form.tags.value + .filter { it.highlight } + .map { it.id }, + alterations = emptyList(), // TODO, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt new file mode 100644 index 0000000..c3055a6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditPage.kt @@ -0,0 +1,394 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.item.edit + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.gestures.scrollBy +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.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBox +import com.pixelized.desktop.lwa.ui.composable.checkbox.LwaCheckBoxUio +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler +import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMItemEditDestination +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagButton +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio +import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors +import com.pixelized.desktop.lwa.ui.theme.lwa +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__action__save +import lwacharactersheet.composeapp.generated.resources.game_master__item__create +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_equipable +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_stackable +import lwacharactersheet.composeapp.generated.resources.game_master__item__edit_consumable +import lwacharactersheet.composeapp.generated.resources.ic_save_24dp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@Stable +data class GMItemEditPageUio( + val id: LwaTextFieldUio, + val label: LwaTextFieldUio, + val description: LwaTextFieldUio, + val thumbnail: LwaTextFieldUio, + val image: LwaTextFieldUio, + val stackable: LwaCheckBoxUio, + val equipable: LwaCheckBoxUio, + val consumable: LwaCheckBoxUio, + val tags: MutableStateFlow>, +) + +@Stable +object GMItemEditDefault { + val paddings = PaddingValues(all = 8.dp) +} + +@Composable +fun GMItemEditPage( + viewModel: GMItemEditViewModel = koinViewModel(), +) { + val screen = LocalScreenController.current + val scope = rememberCoroutineScope() + + val form = viewModel.form.collectAsState() + + GMItemEditContent( + modifier = Modifier.fillMaxSize(), + form = form, + onBack = { + screen.navigateBack() + }, + onSave = { + scope.launch { + if (viewModel.save()) { + screen.navigateBack() + } + } + }, + onTag = { tag -> + viewModel.addTag(tag = tag) + }, + ) + + ErrorSnackHandler( + error = viewModel.error, + ) + + ItemEditKeyHandler( + onDismissRequest = { + screen.navigateBack() + }, + ) +} + +@Composable +private fun GMItemEditContent( + modifier: Modifier = Modifier, + scope: CoroutineScope = rememberCoroutineScope(), + tagsState: LazyListState = rememberLazyListState(), + form: State, + paddings: PaddingValues = GMItemEditDefault.paddings, + onBack: () -> Unit, + onSave: () -> Unit, + onTag: (GMTagUio) -> Unit, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(Res.string.game_master__item__create), + ) + }, + navigationIcon = { + IconButton( + onClick = onBack, + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null, + ) + } + }, + actions = { + TextButton( + onClick = onSave, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + color = MaterialTheme.lwa.colorScheme.base.primary, + fontWeight = FontWeight.SemiBold, + text = stringResource(Res.string.game_master__action__save), + ) + Icon( + painter = painterResource(Res.drawable.ic_save_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null, + ) + } + } + ) + }, + content = { + AnimatedContent( + targetState = form.value, + transitionSpec = { + if (initialState?.id == targetState?.id) { + EnterTransition.None togetherWith ExitTransition.None + } else { + fadeIn() togetherWith fadeOut() + } + } + ) { + when (it) { + null -> Box( + modifier = Modifier.fillMaxSize(), + ) + + else -> { + val tags = it.tags.collectAsState() + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = paddings, + verticalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + item( + key = "Id", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = it.id, + singleLine = true, + ) + } + item( + key = "Name", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = it.label, + singleLine = true, + ) + } + item( + key = "Description", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = it.description, + singleLine = false, + ) + } + item( + key = "Image", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = it.image, + singleLine = false, + ) + } + item( + key = "Thumbnail", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = it.thumbnail, + singleLine = false, + ) + } + item( + key = "Stackable", + ) { + Row( + modifier = Modifier + .animateItem() + .fillMaxWidth().padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + style = MaterialTheme.lwa.typography.base.body1, + text = stringResource(Res.string.game_master__item__edit_stackable) + ) + LwaCheckBox( + field = it.stackable, + ) + } + } + item( + key = "Equipable", + ) { + Row( + modifier = Modifier + .animateItem() + .fillMaxWidth().padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + style = MaterialTheme.lwa.typography.base.body1, + text = stringResource(Res.string.game_master__item__edit_equipable) + ) + LwaCheckBox( + field = it.equipable, + ) + } + } + item( + key = "Consumable", + ) { + Row( + modifier = Modifier + .animateItem() + .fillMaxWidth().padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + style = MaterialTheme.lwa.typography.base.body1, + text = stringResource(Res.string.game_master__item__edit_consumable) + ) + LwaCheckBox( + field = it.consumable, + ) + } + } + item( + key = "Tags", + ) { + LazyRow( + modifier = Modifier.draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { delta -> + scope.launch { + tagsState.scrollBy(-delta) + } + }, + ), + state = tagsState, + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + items( + items = tags.value, + ) { tag -> + GMTagButton( + modifier = Modifier.height(48.dp), + tag = tag, + onTag = { onTag(tag) } + ) + } + } + } + item( + key = "Actions", + ) { + Column( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + horizontalAlignment = Alignment.End + ) { + Button( + colors = LwaButtonColors(), + shape = CircleShape, + onClick = onSave, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + text = stringResource(Res.string.game_master__action__save), + ) + Icon( + painter = painterResource(Res.drawable.ic_save_24dp), + contentDescription = null, + ) + } + } + } + } + } + } + } + }, + ) +} + +@Composable +private fun ItemEditKeyHandler( + onDismissRequest: () -> Unit, +) { + KeyHandler { + when { + it.type == KeyEventType.KeyDown && it.key == Key.Escape -> { + onDismissRequest() + true + } + + else -> false + } + } +} + +private fun NavHostController.navigateBack() = popBackStack( + route = GMItemEditDestination.baseRoute(), + inclusive = true, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditViewModel.kt new file mode 100644 index 0000000..05b3368 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/edit/GMItemEditViewModel.kt @@ -0,0 +1,77 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.item.edit + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.network.LwaNetworkException +import com.pixelized.desktop.lwa.repository.item.ItemRepository +import com.pixelized.desktop.lwa.repository.tag.TagRepository +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMItemEditDestination +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio +import com.pixelized.shared.lwa.protocol.rest.APIResponse.ErrorCode +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class GMItemEditViewModel( + private val itemRepository: ItemRepository, + private val tagRepository: TagRepository, + private val factory: GMItemEditFactory, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val argument = GMItemEditDestination.Argument(savedStateHandle) + + private val _error = MutableSharedFlow() + val error: SharedFlow get() = _error + + private val _form = MutableStateFlow(null) + val form: StateFlow get() = _form + + init { + viewModelScope.launch { + _form.value = factory.createForm( + originId = argument.id, + item = itemRepository.item(itemId = argument.id), + tags = tagRepository.itemsTags(), + ) + } + } + + suspend fun save(): Boolean { + val edited = factory.createItem(form = form.value) ?: return false + + try { + itemRepository.updateItem( + item = edited, + create = argument.id == null, + ) + return true + } catch (exception: LwaNetworkException) { + _form.value?.id?.isError?.value = exception.code == ErrorCode.ItemId + _form.value?.label?.isError?.value = exception.code == ErrorCode.ItemName + + val message = ErrorSnackUio.from(exception = exception) + _error.emit(message) + return false + } catch (exception: Exception) { + val message = ErrorSnackUio.from(exception = exception) + _error.emit(message) + return false + } + } + + fun addTag(tag: GMTagUio) { + _form.value?.tags?.update { tags -> + tags.toMutableList().also { + val index = it.indexOf(tag) + if (index > -1) { + it[index] = tag.copy(highlight = tag.highlight.not()) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItem.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItem.kt new file mode 100644 index 0000000..2b39f6a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItem.kt @@ -0,0 +1,145 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.item.list + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTag +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio +import com.pixelized.desktop.lwa.ui.theme.lwa +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__item__delete +import lwacharactersheet.composeapp.generated.resources.ic_delete_forever_24dp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Stable +data class GMItemUio( + val itemId: String, + val label: String, + val tags: List, +) + +@Stable +object GMItemDefault { + @Stable + val padding = PaddingValues(start = 16.dp) +} + +@Composable +fun GMItem( + modifier: Modifier = Modifier, + padding: PaddingValues = GMItemDefault.padding, + item: GMItemUio, + onItem: () -> Unit, + onDelete: () -> Unit, + onTag: (String) -> Unit, +) { + Row( + modifier = Modifier + .clip(shape = MaterialTheme.lwa.shapes.gameMaster) + .clickable(onClick = onItem) + .background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp) + .minimumInteractiveComponentSize() + .padding(paddingValues = padding) + .then(other = modifier), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + style = MaterialTheme.lwa.typography.base.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = item.label, + ) + Row( + modifier = Modifier.weight(1f).height(intrinsicSize = IntrinsicSize.Min), + horizontalArrangement = Arrangement.spacedBy(space = 2.dp, alignment = Alignment.End) + ) { + item.tags.forEach { tag -> + GMTag( + elevation = 4.dp, + tag = tag, + onTag = { onTag(tag.id) }, + ) + } + } + OverflowActionMenu( + item = item, + onDelete = onDelete, + ) + } +} + +@Composable +private fun OverflowActionMenu( + modifier: Modifier = Modifier, + item: GMItemUio, + onDelete: () -> Unit, +) { + val overflowMenu = remember(item) { + mutableStateOf(false) + } + IconButton( + modifier = modifier, + onClick = { overflowMenu.value = true }, + ) { + Icon( + imageVector = Icons.Default.MoreVert, + tint = MaterialTheme.colors.primary, + contentDescription = null, + ) + DropdownMenu( + expanded = overflowMenu.value, + onDismissRequest = { + overflowMenu.value = false + }, + content = { + DropdownMenuItem( + onClick = { + overflowMenu.value = false + onDelete() + }, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(Res.drawable.ic_delete_forever_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null, + ) + Text( + style = MaterialTheme.lwa.typography.base.body1, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = stringResource(Res.string.game_master__item__delete), + ) + } + } + }, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemFactory.kt new file mode 100644 index 0000000..b48d99c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemFactory.kt @@ -0,0 +1,51 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.item.list + +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory +import com.pixelized.desktop.lwa.utils.extention.unAccent +import com.pixelized.shared.lwa.model.item.Item +import com.pixelized.shared.lwa.model.tag.Tag +import java.text.Collator + +class GMItemFactory( + private val tagFactory: GMTagFactory, +) { + fun filterItem( + items: Collection, + unAccentFilter: String, + selectedTagId: String?, + ): List { + return items.filter { + val matchName = it.metadata.name.unAccent().contains( + other = unAccentFilter, + ignoreCase = true + ) + val matchTag = selectedTagId == null || it.tags.contains( + element = selectedTagId + ) + matchName && matchTag + } + } + + fun convertToGMItemUio( + items: List, + tags: Map, + selectedTagId: String?, + ): List { + return items + .map { item -> + GMItemUio( + itemId = item.id, + label = item.metadata.name, + tags = item.tags.mapNotNull { + tags[it]?.let { tag -> + tagFactory.convertToGMTagItemUio( + tag = tag, + selectedTagId = selectedTagId, + ) + } + } + ) + } + .sortedWith(compareBy(Collator.getInstance()) { it.label }) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemPage.kt new file mode 100644 index 0000000..47f9b30 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemPage.kt @@ -0,0 +1,162 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.item.list + +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.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterItemEditPage +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.GMFilterHeader +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio +import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__item__create +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun GMItemPage( + viewModel: GMItemViewModel = koinViewModel(), +) { + val screen = LocalScreenController.current + val scope = rememberCoroutineScope() + + val items = viewModel.items.collectAsState() + val tags = viewModel.tags.collectAsState() + + Box { + GMItemContent( + modifier = Modifier.fillMaxSize(), + filter = viewModel.filter, + tags = tags, + items = items, + onTag = viewModel::onTag, + onItemEdit = { + screen.navigateToGameMasterItemEditPage(itemId = it) + }, + onItemDelete = { + scope.launch { + viewModel.deleteItem(itemId = it) + } + }, + onItemCreate = { + screen.navigateToGameMasterItemEditPage(itemId = null) + }, + ) + + ErrorSnackHandler( + error = viewModel.error, + ) + } +} + +@Composable +private fun GMItemContent( + modifier: Modifier = Modifier, + padding: Dp = 8.dp, + spacing: Dp = 8.dp, + filter: LwaTextFieldUio, + tags: State>, + items: State>, + onTag: (String) -> Unit, + onItemEdit: (String) -> Unit, + onItemDelete: (String) -> Unit, + onItemCreate: () -> Unit, +) { + Column( + modifier = modifier, + ) { + Surface( + elevation = 1.dp, + ) { + GMFilterHeader( + padding = padding, + spacing = spacing, + filter = filter, + tags = tags, + onTag = onTag, + ) + } + Box( + modifier = Modifier.fillMaxWidth().weight(1f), + ) { + LazyColumn( + modifier = Modifier.matchParentSize(), + contentPadding = remember { + PaddingValues( + start = padding, + top = padding, + end = padding, + bottom = padding + 48.dp + padding, + ) + }, + verticalArrangement = Arrangement.spacedBy(space = spacing), + ) { + items( + items = items.value, + key = { it.itemId }, + ) { item -> + GMItem( + modifier = Modifier + .fillMaxWidth() + .animateItem(), + item = item, + onItem = { + onItemEdit(item.itemId) + }, + onDelete = { + onItemDelete(item.itemId) + }, + onTag = onTag, + ) + } + } + Row( + modifier = Modifier + .align(alignment = Alignment.BottomEnd) + .padding(all = padding), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Button( + colors = LwaButtonColors(), + shape = CircleShape, + onClick = onItemCreate, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + text = stringResource(Res.string.game_master__item__create), + ) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + ) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemViewModel.kt new file mode 100644 index 0000000..0b6f250 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/item/list/GMItemViewModel.kt @@ -0,0 +1,102 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.item.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.repository.item.ItemRepository +import com.pixelized.desktop.lwa.repository.tag.TagRepository +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio +import com.pixelized.desktop.lwa.utils.extention.unAccent +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.runBlocking +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__character__filter +import org.jetbrains.compose.resources.getString + +class GMItemViewModel( + private val itemRepository: ItemRepository, + itemFactory: GMItemFactory, + tagRepository: TagRepository, + tagFactory: GMTagFactory, +) : ViewModel() { + + private val selectedTagId = MutableStateFlow(null) + private val filterValue = MutableStateFlow("") + + private val _error = MutableSharedFlow() + val error: SharedFlow get() = _error + + val filter = LwaTextFieldUio( + enable = true, + label = runBlocking { getString(Res.string.game_master__character__filter) }, + valueFlow = filterValue, + isError = MutableStateFlow(false), + placeHolder = null, + onValueChange = { filterValue.value = it }, + ) + + val tags: StateFlow> = combine( + tagRepository.itemsTagFlow(), + selectedTagId, + ) { tags, selectedTagId -> + tagFactory.convertToGMTagItemUio( + tags = tags.values, + selectedTagId = selectedTagId, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + + val items: StateFlow> = combine( + itemRepository.itemFlow, + tagRepository.itemsTagFlow(), + filter.valueFlow.map { it.unAccent() }, + selectedTagId, + ) { items, tags, unAccentFilter, selectedTagId -> + itemFactory.convertToGMItemUio( + items = itemFactory.filterItem( + items = items.values, + unAccentFilter = unAccentFilter, + selectedTagId = selectedTagId, + ), + tags = tags, + selectedTagId = selectedTagId, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + + fun onTag(id: String) { + selectedTagId.update { + when (it) { + id -> null + else -> id + } + } + } + + suspend fun deleteItem(itemId: String) { + try { + itemRepository.deleteItem( + itemId = itemId, + ) + } catch (exception: Exception) { + val message = ErrorSnackUio.from(exception = exception) + _error.emit(message) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt index 98d9a96..d17fae0 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/levelup/LevelUpViewModel.kt @@ -152,6 +152,7 @@ class LevelUpViewModel( characterSheetRepository.updateCharacter( sheet = levelUpCharacter, + create = false, ) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt index ecf7ca1..e2ab8d9 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/Item.kt @@ -17,5 +17,6 @@ data class Item( data class Options( val stackable: Boolean, val equipable: Boolean, + val consumable: Boolean, ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJsonV1.kt index b8651e7..eb98d3c 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJsonV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/ItemJsonV1.kt @@ -23,5 +23,6 @@ data class ItemJsonV1( data class ItemOptionJsonV1( val stackable: Boolean, val equipable: Boolean, + val consumable: Boolean, ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt index 572dfbd..b99ed1f 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/item/factory/ItemJsonFactoryV1.kt @@ -17,6 +17,7 @@ class ItemJsonFactoryV1 { options = Item.Options( stackable = json.options.stackable, equipable = json.options.equipable, + consumable = json.options.consumable, ), tags = json.tags, alterations = json.alterations, @@ -35,6 +36,7 @@ class ItemJsonFactoryV1 { options = ItemJsonV1.ItemOptionJsonV1( stackable = item.options.stackable, equipable = item.options.equipable, + consumable = item.options.consumable, ), tags = item.tags, alterations = item.alterations,