Add item CRUD into the GameMaster screen
This commit is contained in:
		
							parent
							
								
									4ac30fd9b5
								
							
						
					
					
						commit
						5bcb4367d6
					
				
					 32 changed files with 1467 additions and 40 deletions
				
			
		| 
						 | 
				
			
			@ -232,6 +232,7 @@
 | 
			
		|||
 | 
			
		||||
    <string name="game_master__title">Admin</string>
 | 
			
		||||
    <string name="game_master__action">GameMaster</string>
 | 
			
		||||
    <string name="game_master__action__save">Sauvegarder</string>
 | 
			
		||||
    <string name="game_master__character__filter">Filtrer par nom :</string>
 | 
			
		||||
    <string name="game_master__character_level__label">niv: %1$d</string>
 | 
			
		||||
    <string name="game_master__character_tag__character">Joueur</string>
 | 
			
		||||
| 
						 | 
				
			
			@ -253,7 +254,15 @@
 | 
			
		|||
    <string name="game_master__alteration__edit_tags">Tags</string>
 | 
			
		||||
    <string name="game_master__alteration__edit_field_id">Identifiant du champ</string>
 | 
			
		||||
    <string name="game_master__alteration__edit_field_expression">Expression</string>
 | 
			
		||||
    <string name="game_master__alteration__edit_field_save">Sauvegarder</string>
 | 
			
		||||
    <string name="game_master__alteration__edit_field_cancel">Annuler</string>
 | 
			
		||||
    <string name="game_master__item__create">Créer un objet</string>
 | 
			
		||||
    <string name="game_master__item__delete">Supprimer un objet</string>
 | 
			
		||||
    <string name="game_master__item__edit_id">Identifiant de l'altération</string>
 | 
			
		||||
    <string name="game_master__item__edit_label">Nom</string>
 | 
			
		||||
    <string name="game_master__item__edit_description">Description</string>
 | 
			
		||||
    <string name="game_master__item__edit_image">Image url</string>
 | 
			
		||||
    <string name="game_master__item__edit_thumbnail">Vignette url</string>
 | 
			
		||||
    <string name="game_master__item__edit_stackable">Empilable</string>
 | 
			
		||||
    <string name="game_master__item__edit_equipable">Équipable</string>
 | 
			
		||||
    <string name="game_master__item__edit_consumable">Consommable</string>
 | 
			
		||||
 | 
			
		||||
</resources>
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<Unit>
 | 
			
		||||
 | 
			
		||||
    suspend fun putCharacterDamage(
 | 
			
		||||
| 
						 | 
				
			
			@ -83,12 +85,31 @@ interface LwaClient {
 | 
			
		|||
        characterSheetId: String,
 | 
			
		||||
    ): APIResponse<Unit>
 | 
			
		||||
 | 
			
		||||
    // Items
 | 
			
		||||
 | 
			
		||||
    suspend fun getItems(): APIResponse<List<ItemJson>>
 | 
			
		||||
 | 
			
		||||
    suspend fun getItem(
 | 
			
		||||
        itemId: String,
 | 
			
		||||
    ): APIResponse<ItemJson>
 | 
			
		||||
 | 
			
		||||
    suspend fun putItem(
 | 
			
		||||
        itemJson: ItemJson,
 | 
			
		||||
        create: Boolean,
 | 
			
		||||
    ): APIResponse<Unit>
 | 
			
		||||
 | 
			
		||||
    suspend fun deleteItem(
 | 
			
		||||
        itemId: String,
 | 
			
		||||
    ): APIResponse<Unit>
 | 
			
		||||
 | 
			
		||||
    // Tags
 | 
			
		||||
 | 
			
		||||
    suspend fun getAlterationTags(): APIResponse<List<TagJson>>
 | 
			
		||||
 | 
			
		||||
    suspend fun getCharacterTags(): APIResponse<List<TagJson>>
 | 
			
		||||
 | 
			
		||||
    suspend fun getItemTags(): APIResponse<List<TagJson>>
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun error(error: APIResponse<*>): Nothing = throw LwaNetworkException(error)
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<APIResponse<Unit>>()
 | 
			
		||||
 | 
			
		||||
    @Throws
 | 
			
		||||
    override suspend fun getItems(): APIResponse<List<ItemJson>> = client
 | 
			
		||||
        .get("$root/item/all")
 | 
			
		||||
        .body()
 | 
			
		||||
 | 
			
		||||
    @Throws
 | 
			
		||||
    override suspend fun getItem(
 | 
			
		||||
        itemId: String,
 | 
			
		||||
    ): APIResponse<ItemJson> = client
 | 
			
		||||
        .get("$root/item/detail?itemId=$itemId")
 | 
			
		||||
        .body()
 | 
			
		||||
 | 
			
		||||
    @Throws
 | 
			
		||||
    override suspend fun putItem(
 | 
			
		||||
        item: ItemJson,
 | 
			
		||||
        create: Boolean,
 | 
			
		||||
    ): APIResponse<Unit> = client
 | 
			
		||||
        .put("$root/item/update?create=$create") {
 | 
			
		||||
            contentType(ContentType.Application.Json)
 | 
			
		||||
            setBody(item)
 | 
			
		||||
        }
 | 
			
		||||
        .body<APIResponse<Unit>>()
 | 
			
		||||
 | 
			
		||||
    @Throws
 | 
			
		||||
    override suspend fun deleteItem(
 | 
			
		||||
        itemId: String,
 | 
			
		||||
    ): APIResponse<Unit> = client
 | 
			
		||||
        .delete("$root/item/delete?itemId=$itemId")
 | 
			
		||||
        .body()
 | 
			
		||||
 | 
			
		||||
    @Throws
 | 
			
		||||
    override suspend fun getAlterationTags(): APIResponse<List<TagJson>> = client
 | 
			
		||||
        .get("$root/tag/alteration")
 | 
			
		||||
| 
						 | 
				
			
			@ -152,4 +184,9 @@ class LwaClientImpl(
 | 
			
		|||
    override suspend fun getCharacterTags(): APIResponse<List<TagJson>> = client
 | 
			
		||||
        .get("$root/tag/character")
 | 
			
		||||
        .body()
 | 
			
		||||
 | 
			
		||||
    @Throws
 | 
			
		||||
    override suspend fun getItemTags(): APIResponse<List<TagJson>> = client
 | 
			
		||||
        .get("$root/tag/item")
 | 
			
		||||
        .body()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
    ) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Map<String, Item>>(emptyMap())
 | 
			
		||||
    val items: StateFlow<Map<String, Item>> get() = _items
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val scope = CoroutineScope(Dispatchers.IO + Job())
 | 
			
		||||
        // data update through WebSocket.
 | 
			
		||||
        scope.launch {
 | 
			
		||||
            network.data.collect(::handleMessage)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun items(): Collection<Item> {
 | 
			
		||||
        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<String, Item> {
 | 
			
		||||
        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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,17 @@ class TagRepository(
 | 
			
		|||
 | 
			
		||||
    suspend fun updateAlterationTags() = store.updateAlterationTagsFlow()
 | 
			
		||||
 | 
			
		||||
    suspend fun updateItemTags() = store.updateItemTagsFlow()
 | 
			
		||||
 | 
			
		||||
    fun charactersTagFlow(): StateFlow<Map<String, Tag>> = store.charactersTagFlow()
 | 
			
		||||
 | 
			
		||||
    fun charactersTags(): Collection<Tag> = charactersTagFlow().value.values
 | 
			
		||||
 | 
			
		||||
    fun alterationsTagFlow(): StateFlow<Map<String, Tag>> = store.alterationsTagFlow()
 | 
			
		||||
 | 
			
		||||
    fun alterationsTags(): Collection<Tag> = alterationsTagFlow().value.values
 | 
			
		||||
 | 
			
		||||
    fun itemsTagFlow(): StateFlow<Map<String, Tag>> = store.itemsTagFlow()
 | 
			
		||||
 | 
			
		||||
    fun itemsTags(): Collection<Tag> = itemsTagFlow().value.values
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Map<String, Tag>>(emptyMap())
 | 
			
		||||
    private val alterationTagsFlow = MutableStateFlow<Map<String, Tag>>(emptyMap())
 | 
			
		||||
    private val itemTagsFlow = MutableStateFlow<Map<String, Tag>>(emptyMap())
 | 
			
		||||
 | 
			
		||||
    fun charactersTagFlow(): StateFlow<Map<String, Tag>> = characterTagsFlow
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,9 +36,19 @@ class TagStore(
 | 
			
		|||
        return alterationTagsFlow.value[tagId]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun itemsTagFlow(): StateFlow<Map<String, Tag>> = itemTagsFlow
 | 
			
		||||
 | 
			
		||||
    fun items(): Collection<Tag> {
 | 
			
		||||
        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<String, Tag> {
 | 
			
		||||
        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<String, Tag> {
 | 
			
		||||
        val request = client.getAlterationTags()
 | 
			
		||||
    private suspend inline fun requestTag(
 | 
			
		||||
        crossinline block: suspend () -> APIResponse<List<TagJson>>,
 | 
			
		||||
    ): Map<String, Tag> {
 | 
			
		||||
        val request = block()
 | 
			
		||||
        return when (request.success) {
 | 
			
		||||
            true -> request.data
 | 
			
		||||
                ?.map { factory.convertFromJson(json = it) }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<String>(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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<NavHostController> {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +37,7 @@ fun GameMasterNavHost() {
 | 
			
		|||
            ) {
 | 
			
		||||
                composableGameMasterMainPage()
 | 
			
		||||
                composableGameMasterAlterationEditPage()
 | 
			
		||||
                composableGameMasterItemEditPage()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<GMAlterationEditPageUio?>,
 | 
			
		||||
    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),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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(),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,7 @@ data class GMAlterationUio(
 | 
			
		|||
 | 
			
		||||
@Stable
 | 
			
		||||
object GMAlterationDefault {
 | 
			
		||||
    @Stable
 | 
			
		||||
    val padding = PaddingValues(start = 16.dp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<String?>(null)
 | 
			
		||||
    private val filterValue = MutableStateFlow("")
 | 
			
		||||
 | 
			
		||||
    private val _error = MutableSharedFlow<ErrorSnackUio>()
 | 
			
		||||
    val error: SharedFlow<ErrorSnackUio> 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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Tag>,
 | 
			
		||||
    ): 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,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<List<GMTagUio>>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@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<GMItemEditPageUio?>,
 | 
			
		||||
    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,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -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<ErrorSnackUio>()
 | 
			
		||||
    val error: SharedFlow<ErrorSnackUio> get() = _error
 | 
			
		||||
 | 
			
		||||
    private val _form = MutableStateFlow<GMItemEditPageUio?>(null)
 | 
			
		||||
    val form: StateFlow<GMItemEditPageUio?> 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())
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<GMTagUio>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@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),
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<Item>,
 | 
			
		||||
        unAccentFilter: String,
 | 
			
		||||
        selectedTagId: String?,
 | 
			
		||||
    ): List<Item> {
 | 
			
		||||
        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<Item>,
 | 
			
		||||
        tags: Map<String, Tag>,
 | 
			
		||||
        selectedTagId: String?,
 | 
			
		||||
    ): List<GMItemUio> {
 | 
			
		||||
        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 })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<List<GMTagUio>>,
 | 
			
		||||
    items: State<List<GMItemUio>>,
 | 
			
		||||
    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,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<String?>(null)
 | 
			
		||||
    private val filterValue = MutableStateFlow("")
 | 
			
		||||
 | 
			
		||||
    private val _error = MutableSharedFlow<ErrorSnackUio>()
 | 
			
		||||
    val error: SharedFlow<ErrorSnackUio> 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<List<GMTagUio>> = combine(
 | 
			
		||||
        tagRepository.itemsTagFlow(),
 | 
			
		||||
        selectedTagId,
 | 
			
		||||
    ) { tags, selectedTagId ->
 | 
			
		||||
        tagFactory.convertToGMTagItemUio(
 | 
			
		||||
            tags = tags.values,
 | 
			
		||||
            selectedTagId = selectedTagId,
 | 
			
		||||
        )
 | 
			
		||||
    }.stateIn(
 | 
			
		||||
        scope = viewModelScope,
 | 
			
		||||
        started = SharingStarted.Lazily,
 | 
			
		||||
        initialValue = emptyList(),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    val items: StateFlow<List<GMItemUio>> = 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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +152,7 @@ class LevelUpViewModel(
 | 
			
		|||
 | 
			
		||||
        characterSheetRepository.updateCharacter(
 | 
			
		||||
            sheet = levelUpCharacter,
 | 
			
		||||
            create = false,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -17,5 +17,6 @@ data class Item(
 | 
			
		|||
    data class Options(
 | 
			
		||||
        val stackable: Boolean,
 | 
			
		||||
        val equipable: Boolean,
 | 
			
		||||
        val consumable: Boolean,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -23,5 +23,6 @@ data class ItemJsonV1(
 | 
			
		|||
    data class ItemOptionJsonV1(
 | 
			
		||||
        val stackable: Boolean,
 | 
			
		||||
        val equipable: Boolean,
 | 
			
		||||
        val consumable: Boolean,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue