diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 4f42237..3501227 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -243,6 +243,16 @@ Retirer des Npcs Créer un personnage Filtrer par nom : + Créer une altération Supprimer l'altération + Ajouter un champ + Identifiant de l'altération + Nom + Description + Tags + Identifiant du champ + Expression + Sauvegarder + Annuler \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt index f4240a3..349d046 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/DataSyncViewModel.kt @@ -41,7 +41,9 @@ class DataSyncViewModel( .filter { status -> status == NetworkRepository.Status.CONNECTED } .onEach { alterationRepository.updateAlterations() + alterationRepository.updateTags() characterRepository.updateCharacterPreviews() + characterRepository.updateTags() campaignRepository.updateCampaign() } .launchIn(this) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt index 07122be..9d9574f 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -36,10 +36,13 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEdi import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionViewModel -import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.GMAlterationFactory -import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.GMAlterationViewModel -import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.GMCharacterFactory -import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.GMCharacterViewModel +import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditFactory +import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.list.GMAlterationFactory +import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.list.GMAlterationViewModel +import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditViewModel +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.levelup.LevelUpFactory import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpViewModel import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryViewModel @@ -123,8 +126,10 @@ val factoryDependencies factoryOf(::CharacterSheetDiminishedDialogFactory) factoryOf(::TextMessageFactory) factoryOf(::LevelUpFactory) + factoryOf(::GMTagFactory) factoryOf(::GMCharacterFactory) factoryOf(::GMAlterationFactory) + factoryOf(::GMAlterationEditFactory) } val viewModelDependencies @@ -149,6 +154,7 @@ val viewModelDependencies viewModelOf(::GameMasterViewModel) viewModelOf(::GMActionViewModel) viewModelOf(::GMAlterationViewModel) + viewModelOf(::GMAlterationEditViewModel) } val useCaseDependencies diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt index af869e5..24e8365 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClient.kt @@ -3,6 +3,7 @@ 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.tag.TagJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson interface LwaClient { @@ -15,6 +16,16 @@ interface LwaClient { alterationId: String, ): AlterationJson? + suspend fun updateAlteration( + alterationJson: AlterationJson + ) + + suspend fun deleteAlteration( + alterationId: String, + ) + + suspend fun alterationTags(): List + // Campaign suspend fun campaign(): CampaignJson @@ -39,6 +50,8 @@ interface LwaClient { suspend fun characters(): List + suspend fun characterTags(): List + suspend fun character( characterSheetId: String, ): CharacterSheetJson diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index 082271c..62d2449 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt @@ -4,6 +4,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.shared.lwa.model.alteration.AlterationJson import com.pixelized.shared.lwa.model.campaign.CampaignJson import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson +import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson import io.ktor.client.HttpClient import io.ktor.client.call.body @@ -30,6 +31,23 @@ class LwaClientImpl( .get("$root/alteration/detail?alterationId=$alterationId") .body() + override suspend fun updateAlteration( + alterationJson: AlterationJson + ) = client + .put("$root/alteration/update") { + contentType(ContentType.Application.Json) + setBody(alterationJson) + } + .body() + + override suspend fun deleteAlteration(alterationId: String) = client + .delete("$root/alteration/delete?alterationId=$alterationId") + .body() + + override suspend fun alterationTags(): List = client + .get("$root/alteration/tags") + .body() + override suspend fun campaign(): CampaignJson = client .get("$root/campaign") .body() @@ -62,6 +80,10 @@ class LwaClientImpl( .get("$root/character/all") .body() + override suspend fun characterTags(): List = client + .get("$root/character/tags") + .body() + override suspend fun character( characterSheetId: String, ): CharacterSheetJson = client diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt index 498d74f..7687eaa 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationRepository.kt @@ -26,6 +26,8 @@ class AlterationRepository( val alterationFlow get() = alterationStore.alterationsFlow + val tagsFlow get() = alterationStore.tagsFlow + /** * This flow transform the campaign instance (player + npc) into a * Map>. @@ -67,6 +69,16 @@ class AlterationRepository( alterationStore.updateAlterations() } + suspend fun updateTags() { + alterationStore.updateTags() + } + + fun alteration( + alterationId: String?, + ): Alteration? { + return alterationFlow.value[alterationId] + } + fun fieldAlterations( characterSheetId: String, ): Map> { @@ -79,6 +91,18 @@ class AlterationRepository( return activeAlterationMapFlow.map { it[characterSheetId] ?: emptyMap() } } + suspend fun updateAlteration( + alteration: Alteration + ) { + alterationStore.putAlteration(alteration) + } + + suspend fun deleteAlteration( + alterationId: String, + ) { + alterationStore.deleteAlteration(alterationId = alterationId) + } + private fun transformToAlterationFieldMap( alterations: Map, actives: List, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt index 863b19f..1924f74 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/alteration/AlterationStore.kt @@ -1,32 +1,92 @@ package com.pixelized.desktop.lwa.repository.alteration import com.pixelized.desktop.lwa.network.LwaClient +import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.shared.lwa.model.alteration.Alteration import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory +import com.pixelized.shared.lwa.model.tag.Tag +import com.pixelized.shared.lwa.model.tag.TagJsonFactory +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 AlterationStore( + private val networkRepository: NetworkRepository, private val alterationFactory: AlterationJsonFactory, + private val tagFactory: TagJsonFactory, private val client: LwaClient, ) { private val _alterationsFlow = MutableStateFlow>(emptyMap()) val alterationsFlow: StateFlow> = _alterationsFlow + private val _tagsFlow = MutableStateFlow>(emptyMap()) + val tagsFlow: StateFlow> = _tagsFlow + init { val scope = CoroutineScope(Dispatchers.IO + Job()) scope.launch { updateAlterations() + updateTags() } + // data update through WebSocket. + scope.launch { + networkRepository.data.collect(::handleMessage) + } + } + + fun alterations(): Collection { + return alterationsFlow.value.values + } + + fun tags(): Collection { + return tagsFlow.value.values + } + + fun alteration(alterationId: String): Alteration? { + return alterationsFlow.value[alterationId] + } + + fun tag(tagId: String): Tag? { + return tagsFlow.value[tagId] } suspend fun updateAlterations() { _alterationsFlow.value = try { - loadAlteration() + getAlteration() + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + emptyMap() + } + } + + suspend fun updateAlteration( + alterationId: String, + ) { + val alteration = try { + getAlteration(alterationId = alterationId) + } catch (exception: Exception) { + println(exception.message) // TODO proper exception handling + null + } + + if (alteration == null) return + + _alterationsFlow.update { alterations -> + alterations.toMutableMap().also { + it[alterationId] = alteration + } + } + } + + suspend fun updateTags() { + _tagsFlow.value = try { + getAlterationTag() } catch (exception: Exception) { println(exception.message) // TODO proper exception handling emptyMap() @@ -34,17 +94,62 @@ class AlterationStore( } @Throws - private suspend fun loadAlteration(): Map { + private suspend fun getAlteration(): Map { val request = client.alterations() val data = request.map { alterationFactory.convertFromJson(json = it) } return data.associateBy { it.id } } - fun alterations(): Collection { - return alterationsFlow.value.values + @Throws + private suspend fun getAlteration( + alterationId: String, + ): Alteration? { + val request = client.alterations(alterationId = alterationId) + return request?.let { alterationFactory.convertFromJson(json = it) } } - fun alteration(alterationId: String): Alteration? { - return alterationsFlow.value[alterationId] + @Throws + private suspend fun getAlterationTag(): Map { + val request = client.alterationTags() + val data = request.map { tagFactory.convertFromJson(json = it) } + return data.associateBy { it.id } } + + @Throws + suspend fun putAlteration( + alteration: Alteration, + ) { + client.updateAlteration( + alterationJson = alterationFactory.convertToJson(data = alteration) + ) + } + + @Throws + suspend fun deleteAlteration( + alterationId: String + ) { + client.deleteAlteration( + alterationId = alterationId + ) + } + + // region: WebSocket & data update. + + private suspend fun handleMessage(message: SocketMessage) { + when (message) { + is ApiSynchronisation.AlterationUpdate -> updateAlteration( + alterationId = message.alterationId, + ) + + is ApiSynchronisation.AlterationDelete -> _alterationsFlow.update { alterations -> + alterations.toMutableMap().also { + it.remove(message.alterationId) + } + } + + else -> Unit + } + } + + // endregion } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt index 6f4909f..34f1f3d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetRepository.kt @@ -19,9 +19,14 @@ class CharacterSheetRepository( val characterSheetPreviewFlow get() = store.previewFlow val characterDetailFlow get() = store.detailFlow + val tagsFlow get() = store.tagsFlow suspend fun updateCharacterPreviews() { - store.charactersPreview() + store.updateCharactersPreview() + } + + suspend fun updateTags() { + store.updateTags() } fun characterPreview(characterId: String?): CharacterSheetPreview? { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt index 20aa0d5..def69ff 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/characterSheet/CharacterSheetStore.kt @@ -6,6 +6,8 @@ import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview import com.pixelized.shared.lwa.model.characterSheet.factory.CharacterSheetJsonFactory +import com.pixelized.shared.lwa.model.tag.Tag +import com.pixelized.shared.lwa.model.tag.TagJsonFactory import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation import com.pixelized.shared.lwa.protocol.websocket.CharacterSheetEvent import com.pixelized.shared.lwa.protocol.websocket.SocketMessage @@ -23,7 +25,7 @@ class CharacterSheetStore( private val client: LwaClient, private val network: NetworkRepository, private val factory: CharacterSheetJsonFactory, - private val useCase: CharacterSheetUseCase, + private val tagFactory: TagJsonFactory, ) { private val _previewFlow = MutableStateFlow>(value = emptyList()) val previewFlow: StateFlow> get() = _previewFlow @@ -31,11 +33,15 @@ class CharacterSheetStore( private val _detailFlow = MutableStateFlow>(value = emptyMap()) val detailFlow: StateFlow> get() = _detailFlow + private val _tagsFlow = MutableStateFlow>(emptyMap()) + val tagsFlow: StateFlow> = _tagsFlow + init { val scope = CoroutineScope(Dispatchers.IO + Job()) // initial data loading. scope.launch { - charactersPreview() + updateCharactersPreview() + updateTags() } // data update through WebSocket. scope.launch { @@ -45,7 +51,7 @@ class CharacterSheetStore( // region Rest - suspend fun charactersPreview(): List { + suspend fun updateCharactersPreview(): List { val request = try { client.characters() } catch (exception: Exception) { @@ -58,6 +64,21 @@ class CharacterSheetStore( return _previewFlow.update(characters) } + suspend fun updateTags(): Map { + val request = try { + client.characterTags() + } catch (exception: Exception) { + println(exception) // TODO proper exception handling + emptyList() + } + val tags = request.map { + tagFactory.convertFromJson(json = it) + }.associateBy { it.id } + + _tagsFlow.value = tags + return tags + } + suspend fun getCharacterSheet( characterSheetId: String, forceUpdate: Boolean = false, @@ -110,7 +131,7 @@ class CharacterSheetStore( forceUpdate = true, ) if (_previewFlow.value.firstOrNull { it.characterSheetId == message.characterSheetId } == null) { - charactersPreview() + updateCharactersPreview() } } @@ -126,6 +147,9 @@ class CharacterSheetStore( sheets.toMutableMap().also { it.remove(message.characterSheetId) } } } + + is ApiSynchronisation.AlterationUpdate -> Unit + is ApiSynchronisation.AlterationDelete -> Unit } is CharacterSheetEvent -> when (message) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextField.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextField.kt index 42e70e9..ada79c6 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextField.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/textfield/LwaTextField.kt @@ -20,9 +20,9 @@ import kotlinx.coroutines.flow.StateFlow data class LwaTextFieldUio( val enable: Boolean = true, val isError: StateFlow, - val labelFlow: StateFlow, + val labelFlow: StateFlow?, val valueFlow: StateFlow, - val placeHolderFlow: StateFlow, + val placeHolderFlow: StateFlow?, val onValueChange: (String) -> Unit, ) @@ -43,9 +43,9 @@ fun LwaTextField( Modifier } - val label = field.labelFlow.collectAsState() + val label = field.labelFlow?.collectAsState() + val placeHolder = field.placeHolderFlow?.collectAsState() val value = field.valueFlow.collectAsState() - val placeHolder = field.placeHolderFlow.collectAsState() val isError = field.isError.collectAsState() TextField( @@ -56,7 +56,7 @@ fun LwaTextField( }, enabled = field.enable, singleLine = singleLine, - placeholder = placeHolder.value?.let { + placeholder = placeHolder?.value?.let { { Text( overflow = TextOverflow.Ellipsis, @@ -66,7 +66,7 @@ fun LwaTextField( } }, isError = isError.value, - label = label.value?.let { + label = label?.value?.let { { Text( overflow = TextOverflow.Ellipsis, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetEditDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetEditDestination.kt index d174e11..b7dfaf3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetEditDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/CharacterSheetEditDestination.kt @@ -1,5 +1,6 @@ package com.pixelized.desktop.lwa.ui.navigation.screen.destination +import androidx.compose.runtime.Stable import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController @@ -15,8 +16,7 @@ object CharacterSheetEditDestination { fun baseRoute() = "$ROUTE?${CHARACTER_ID.ARG}" - fun navigationRoute(characterSheetId: String?) = ROUTE + - "?$CHARACTER_ID=$characterSheetId" + fun navigationRoute(characterSheetId: String?) = "$ROUTE?$CHARACTER_ID=$characterSheetId" fun arguments() = listOf( navArgument(CHARACTER_ID) { @@ -25,6 +25,7 @@ object CharacterSheetEditDestination { }, ) + @Stable data class Argument( val id: String?, ) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMAlterationDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMAlterationDestination.kt index eb5dc86..6e1e2cb 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMAlterationDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMAlterationDestination.kt @@ -3,7 +3,7 @@ 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.alteration.GMAlterationPage +import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.list.GMAlterationPage object GMAlterationDestination { private const val ROUTE = "GameMasterAlteration" diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMAlterationEditDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMAlterationEditDestination.kt new file mode 100644 index 0000000..8bdd8f9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMAlterationEditDestination.kt @@ -0,0 +1,58 @@ +package com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster + +import androidx.compose.runtime.Stable +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit.GMAlterationEditPage +import com.pixelized.desktop.lwa.utils.extention.ARG + +@Stable +object GMAlterationEditDestination { + private const val ROUTE = "GameMasterAlterationEdit" + private const val ALTERATION_ID = "id" + + @Stable + fun baseRoute() = "$ROUTE?${ALTERATION_ID.ARG}" + + @Stable + fun navigationRoute(alterationId: String?) = "$ROUTE?$ALTERATION_ID=$alterationId" + + @Stable + fun arguments() = listOf( + navArgument(ALTERATION_ID) { + nullable = true + type = NavType.StringType + }, + ) + + @Stable + data class Argument( + val id: String?, + ) { + constructor(savedStateHandle: SavedStateHandle) : this( + id = savedStateHandle.get(ALTERATION_ID), + ) + } +} + +fun NavGraphBuilder.composableGameMasterAlterationEditPage() { + composable( + route = GMAlterationEditDestination.baseRoute(), + arguments = GMAlterationEditDestination.arguments(), + ) { + GMAlterationEditPage() + } +} + +fun NavHostController.navigateToGameMasterAlterationEditPage( + alterationId: String?, +) { + val route = GMAlterationEditDestination.navigationRoute( + alterationId = alterationId, + ) + navigate(route = route) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMCharacterDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMCharacterDestination.kt index a21a801..b1ff699 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMCharacterDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMCharacterDestination.kt @@ -3,7 +3,7 @@ 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.character.GMCharacterPage +import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list.GMCharacterPage object GMCharacterDestination { private const val ROUTE = "GameMasterCharacter" diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt index 2e78df7..8b7b6ea 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/GameMasterScreen.kt @@ -20,27 +20,18 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -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.LayoutDirection import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController -import com.pixelized.desktop.lwa.LocalBlurController -import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel -import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedViewModel -import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMActionDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterActionPage +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterAlterationEditPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterAlterationPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterCharacterPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterObjectPage @@ -48,11 +39,8 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.nav import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterAlterationPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterCharacterPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterObjectPage -import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanel -import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTab -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTabUio +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.GMTab +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.GMTabUio import com.pixelized.desktop.lwa.ui.theme.color.component.LwaSwitchColors import com.pixelized.desktop.lwa.ui.theme.lwa import lwacharactersheet.composeapp.generated.resources.Res @@ -175,6 +163,7 @@ private fun GameMasterContent( composableGameMasterActionPage() composableGameMasterCharacterPage() composableGameMasterAlterationPage() + composableGameMasterAlterationEditPage() composableGameMasterObjectPage() } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMAction.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMAction.kt similarity index 92% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMAction.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMAction.kt index 6bb44d1..a2a26b3 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMAction.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMAction.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.items +package com.pixelized.desktop.lwa.ui.screen.gamemaster.action import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -6,10 +6,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.runtime.Composable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt index cf3305c..52027fc 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/action/GMActionPage.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMAction import kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditFactory.kt new file mode 100644 index 0000000..14e5d51 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditFactory.kt @@ -0,0 +1,115 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit + +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +import com.pixelized.shared.lwa.model.alteration.Alteration +import com.pixelized.shared.lwa.parser.expression.ExpressionParser +import kotlinx.coroutines.flow.MutableStateFlow +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_expression +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_id +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_id +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_label +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_description +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_tags +import org.jetbrains.compose.resources.getString +import java.util.UUID + +class GMAlterationEditFactory( + private val expressionParser: ExpressionParser, +) { + suspend fun createForm( + alteration: Alteration?, + ): GMAlterationEditPageUio { + val id = MutableStateFlow(alteration?.id ?: "") + val label = MutableStateFlow(alteration?.metadata?.name ?: "") + val description = MutableStateFlow(alteration?.metadata?.description ?: "") + val tags = MutableStateFlow(alteration?.tags?.joinToString(", ") { it } ?: "") + val fields = MutableStateFlow(alteration?.fields?.map { createField(it) } ?: listOf(createField(null))) + + return GMAlterationEditPageUio( + id = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + labelFlow = MutableStateFlow(getString(Res.string.game_master__alteration__edit_id)), + valueFlow = id, + placeHolderFlow = null, + onValueChange = { id.value = it }, + ), + label = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + labelFlow = MutableStateFlow(getString(Res.string.game_master__alteration__edit_label)), + valueFlow = label, + placeHolderFlow = null, + onValueChange = { label.value = it }, + ), + description = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + labelFlow = MutableStateFlow(getString(Res.string.game_master__alteration__edit_description)), + valueFlow = description, + placeHolderFlow = null, + onValueChange = { description.value = it }, + ), + tags = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + labelFlow = MutableStateFlow(getString(Res.string.game_master__alteration__edit_tags)), + valueFlow = tags, + placeHolderFlow = null, + onValueChange = { tags.value = it }, + ), + fields = fields, + ) + } + + suspend fun createField( + alteration: Alteration.Field?, + ): GMAlterationEditPageUio.SkillUio { + val idFlow = MutableStateFlow(alteration?.fieldId ?: "") + val expressionFlow = MutableStateFlow(alteration?.expression?.toString() ?: "") + + return GMAlterationEditPageUio.SkillUio( + key = "${UUID.randomUUID()}-${System.currentTimeMillis()}", + id = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + labelFlow = MutableStateFlow(getString(Res.string.game_master__alteration__edit_field_id)), + valueFlow = idFlow, + placeHolderFlow = null, + onValueChange = { idFlow.value = it }, + ), + expression = LwaTextFieldUio( + enable = true, + isError = MutableStateFlow(false), + labelFlow = MutableStateFlow(getString(Res.string.game_master__alteration__edit_field_expression)), + valueFlow = expressionFlow, + placeHolderFlow = null, + onValueChange = { expressionFlow.value = it }, + ) + ) + } + + suspend fun createAlteration( + form: GMAlterationEditPageUio?, + ): Alteration? { + if (form == null) return null + + return Alteration( + id = form.id.valueFlow.value, + metadata = Alteration.MetaData( + name = form.label.valueFlow.value, + description = form.description.valueFlow.value, + ), + tags = form.tags.valueFlow.value.split(","), + fields = form.fields.value.mapNotNull { field -> + expressionParser.parse(input = field.expression.valueFlow.value)?.let { + Alteration.Field( + fieldId = field.id.valueFlow.value, + expression = it + ) + } + } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditPage.kt new file mode 100644 index 0000000..84716ee --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditPage.kt @@ -0,0 +1,280 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +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.itemsIndexed +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.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +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.unit.dp +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.theme.color.component.LwaButtonColors +import com.pixelized.desktop.lwa.ui.theme.lwa +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_add_field +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_cancel +import lwacharactersheet.composeapp.generated.resources.game_master__alteration__edit_field_save +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 GMAlterationEditPageUio( + val id: LwaTextFieldUio, + val label: LwaTextFieldUio, + val description: LwaTextFieldUio, + val tags: LwaTextFieldUio, + val fields: MutableStateFlow>, +) { + @Stable + data class SkillUio( + val key: String, + val id: LwaTextFieldUio, + val expression: LwaTextFieldUio, + ) +} + +@Stable +object GMAlterationEditPageDefault { + val paddings = PaddingValues(all = 8.dp) +} + +@Composable +fun GMAlterationEditPage( + viewModel: GMAlterationEditViewModel = koinViewModel(), +) { + val screen = LocalScreenController.current + val scope = rememberCoroutineScope() + val form = viewModel.form.collectAsState() + + AnimatedContent( + targetState = form.value, + transitionSpec = { fadeIn() togetherWith fadeOut() } + ) { + when (it) { + null -> Box(modifier = Modifier.fillMaxSize()) + else -> GMAlterationEditContent( + modifier = Modifier.fillMaxSize(), + form = it, + paddings = GMAlterationEditPageDefault.paddings, + addField = { + scope.launch { + viewModel.addField() + } + }, + removeField = viewModel::removeField, + onSave = { + scope.launch { + viewModel.save() + } + }, + onCancel = { + screen.popBackStack() + }, + ) + } + } + + AlterationEditKeyHandler( + onDismissRequest = { + screen.popBackStack() + } + ) +} + +@Composable +private fun GMAlterationEditContent( + modifier: Modifier = Modifier, + form: GMAlterationEditPageUio, + paddings: PaddingValues, + addField: () -> Unit, + removeField: (index: Int) -> Unit, + onSave: () -> Unit, + onCancel: () -> Unit, +) { + val fields = form.fields.collectAsState() + + LazyColumn( + modifier = modifier, + contentPadding = paddings, + verticalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + item( + key = "Id", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = form.id, + singleLine = true, + ) + } + item( + key = "Name", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = form.label, + singleLine = true, + ) + } + item( + key = "Description", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = form.description, + singleLine = false, + ) + } + item( + key = "Tags", + ) { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + field = form.tags, + singleLine = true, + ) + } + itemsIndexed( + items = fields.value, + key = { _, item -> item.key }, + ) { index, item -> + Row( + modifier = Modifier.animateItem(), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LwaTextField( + modifier = Modifier.weight(1f), + field = item.id, + singleLine = true, + ) + LwaTextField( + modifier = Modifier.weight(1f), + field = item.expression, + singleLine = true, + ) + IconButton( + onClick = { removeField(index) }, + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + tint = MaterialTheme.lwa.colorScheme.base.primary, + ) + } + } + } + item( + key = "Actions", + ) { + Column( + modifier = Modifier + .animateItem() + .fillMaxWidth(), + horizontalAlignment = Alignment.End + ) { + Button( + colors = LwaButtonColors(), + shape = CircleShape, + onClick = addField, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + text = stringResource(Res.string.game_master__alteration__edit_add_field), + ) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + ) + } + Button( + colors = LwaButtonColors(), + shape = CircleShape, + onClick = onSave, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + text = stringResource(Res.string.game_master__alteration__edit_field_save), + ) + Icon( + painter = painterResource(Res.drawable.ic_save_24dp), + contentDescription = null, + ) + } + Button( + colors = LwaButtonColors(), + shape = CircleShape, + onClick = onCancel, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + text = stringResource(Res.string.game_master__alteration__edit_field_cancel), + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + } + } + } +} + +@Composable +private fun AlterationEditKeyHandler( + onDismissRequest: () -> Unit, +) { + KeyHandler { + when { + it.type == KeyEventType.KeyUp && it.key == Key.Escape -> { + onDismissRequest() + true + } + + else -> false + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditViewModel.kt new file mode 100644 index 0000000..6b706a1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/edit/GMAlterationEditViewModel.kt @@ -0,0 +1,58 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.edit + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMAlterationEditDestination +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class GMAlterationEditViewModel( + private val alterationRepository: AlterationRepository, + private val factory: GMAlterationEditFactory, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val argument = GMAlterationEditDestination.Argument(savedStateHandle) + + private val _form = MutableStateFlow(null) + val form: StateFlow get() = _form + + init { + viewModelScope.launch { + _form.value = factory.createForm( + alteration = alterationRepository.alteration(alterationId = argument.id) + ) + } + } + + suspend fun save() { + val edited = factory.createAlteration(form = form.value) + + val actual = alterationRepository.alterationFlow.value[edited?.id] + + // TODO if argument.id == null et actual?.id != null on créer et on écrase existant !!! + + if (edited != null) + + alterationRepository.updateAlteration(edited) + } + + suspend fun addField() { + form.value?.fields?.update { fields -> + fields.toMutableList().also { + it.add(element = factory.createField(alteration = null)) + } + } + } + + fun removeField(index: Int) { + form.value?.fields?.update { fields -> + fields.toMutableList().also { + it.removeAt(index = index) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMAlterationUio.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlteration.kt similarity index 95% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMAlterationUio.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlteration.kt index 2ab1c93..805c5d7 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMAlterationUio.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlteration.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.items +package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.list import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -26,6 +26,8 @@ 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__alteration__delete @@ -37,7 +39,7 @@ import org.jetbrains.compose.resources.stringResource data class GMAlterationUio( val alterationId: String, val label: String, - val tags: List, + val tags: List, ) @Stable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationFactory.kt similarity index 61% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationFactory.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationFactory.kt index 2e72280..2670f5c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationFactory.kt @@ -1,12 +1,14 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration +package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.list -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMAlterationUio -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio +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.alteration.Alteration +import com.pixelized.shared.lwa.model.tag.Tag import java.text.Collator -class GMAlterationFactory { +class GMAlterationFactory( + private val tagFactory: GMTagFactory, +) { fun filterAlteration( alterations: Collection, @@ -27,6 +29,7 @@ class GMAlterationFactory { fun convertToGMAlterationUio( alterations: List, + tags: Map, selectedTagId: String?, ): List { return alterations @@ -34,28 +37,16 @@ class GMAlterationFactory { GMAlterationUio( alterationId = alteration.id, label = alteration.metadata.name, - tags = alteration.tags.map { tag -> - GMTagItemUio( - id = tag, - label = tag, - highlight = tag == selectedTagId, - ) + tags = alteration.tags.mapNotNull { + tags[it]?.let { tag -> + tagFactory.convertToGMTagItemUio( + tag = tag, + selectedTagId = selectedTagId, + ) + } } ) } .sortedWith(compareBy(Collator.getInstance()) { it.label }) } - - fun convertToGMTagItemUio( - alterationTagIds: List, - selectedTagId: String?, - ): List { - return alterationTagIds.map { - GMTagItemUio( - id = it, - label = it, - highlight = it == selectedTagId, - ) - } - } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationPage.kt similarity index 80% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationPage.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationPage.kt index 2b93490..c03eb48 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationPage.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration +package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.list import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -21,17 +21,20 @@ 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.textfield.LwaTextFieldUio -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMAlteration -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMAlterationUio -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMFilterHeader -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio +import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterAlterationEditPage +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__alteration__create import lwacharactersheet.composeapp.generated.resources.game_master__create_character_sheet import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel @@ -40,8 +43,11 @@ import org.koin.compose.viewmodel.koinViewModel fun GMAlterationPage( viewModel: GMAlterationViewModel = koinViewModel(), ) { + val screen = LocalScreenController.current + val scope = rememberCoroutineScope() + val alterations = viewModel.alterations.collectAsState() - val tags = viewModel.alterationTags.collectAsState() + val tags = viewModel.tags.collectAsState() Box { GMAlterationContent( @@ -50,9 +56,17 @@ fun GMAlterationPage( tags = tags, alterations = alterations, onTag = viewModel::onTag, - onAlterationEdit = { }, - onAlterationDelete = { }, - onAlterationCreate = { }, + onAlterationEdit = { + screen.navigateToGameMasterAlterationEditPage(alterationId = it) + }, + onAlterationDelete = { + scope.launch { + viewModel.deleteAlteration(alterationId = it) + } + }, + onAlterationCreate = { + screen.navigateToGameMasterAlterationEditPage(alterationId = null) + }, ) } } @@ -63,7 +77,7 @@ private fun GMAlterationContent( padding: Dp = 8.dp, spacing: Dp = 8.dp, filter: LwaTextFieldUio, - tags: State>, + tags: State>, alterations: State>, onTag: (String) -> Unit, onAlterationEdit: (String) -> Unit, @@ -131,7 +145,7 @@ private fun GMAlterationContent( ) { Text( modifier = Modifier.padding(end = 4.dp), - text = stringResource(Res.string.game_master__create_character_sheet), + text = stringResource(Res.string.game_master__alteration__create), ) Icon( imageVector = Icons.Default.Add, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationViewModel.kt similarity index 64% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationViewModel.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationViewModel.kt index b78cf18..b12767d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/GMAlterationViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/alteration/list/GMAlterationViewModel.kt @@ -1,17 +1,17 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration +package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.list import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository 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.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking @@ -20,33 +20,29 @@ import lwacharactersheet.composeapp.generated.resources.game_master__character__ import org.jetbrains.compose.resources.getString class GMAlterationViewModel( - alterationRepository: AlterationRepository, + private val alterationRepository: AlterationRepository, alterationFactory: GMAlterationFactory, + tagFactory: GMTagFactory, ) : ViewModel() { - private val _filter = MutableStateFlow("") + private val selectedTagId = MutableStateFlow(null) + private val filterValue = MutableStateFlow("") + val filter = LwaTextFieldUio( enable = true, labelFlow = MutableStateFlow(runBlocking { getString(Res.string.game_master__character__filter) }), - valueFlow = _filter, + valueFlow = filterValue, isError = MutableStateFlow(false), placeHolderFlow = MutableStateFlow(null), - onValueChange = { _filter.value = it }, + onValueChange = { filterValue.value = it }, ) - @OptIn(ExperimentalCoroutinesApi::class) - private val alterationTagIds = alterationRepository.alterationFlow - .mapLatest { alterations -> alterations.values.flatMap { it.tags }.toSet().toList() } - .distinctUntilChanged() - - private val selectedTagId = MutableStateFlow(null) - - val alterationTags = combine( - alterationTagIds, + val tags: StateFlow> = combine( + alterationRepository.tagsFlow, selectedTagId, - ) { alterationTagIds, selectedTagId -> - alterationFactory.convertToGMTagItemUio( - alterationTagIds = alterationTagIds, + ) { tags, selectedTagId -> + tagFactory.convertToGMTagItemUio( + tags = tags.values, selectedTagId = selectedTagId, ) }.stateIn( @@ -55,22 +51,20 @@ class GMAlterationViewModel( initialValue = emptyList(), ) - @OptIn(ExperimentalCoroutinesApi::class) - val alterations = combine( + val alterations: StateFlow> = combine( alterationRepository.alterationFlow, + alterationRepository.tagsFlow, filter.valueFlow.map { it.unAccent() }, selectedTagId, - transform = { alterations, unAccentFilter, selectedTagId -> - alterationFactory.filterAlteration( + ) { alterations, tags, unAccentFilter, selectedTagId -> + alterationFactory.convertToGMAlterationUio( + alterations = alterationFactory.filterAlteration( alterations = alterations.values, unAccentFilter = unAccentFilter, - selectedTagId = selectedTagId - ) - } - ).mapLatest { alterations -> - alterationFactory.convertToGMAlterationUio( - alterations = alterations, - selectedTagId = selectedTagId.value + selectedTagId = selectedTagId, + ), + tags = tags, + selectedTagId = selectedTagId, ) }.stateIn( scope = viewModelScope, @@ -86,4 +80,8 @@ class GMAlterationViewModel( } } } + + suspend fun deleteAlteration(alterationId: String) { + alterationRepository.deleteAlteration(alterationId) + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterFactory.kt deleted file mode 100644 index 8d02775..0000000 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterFactory.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.character - -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio.Action -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio -import com.pixelized.desktop.lwa.utils.extention.unAccent -import com.pixelized.shared.lwa.model.campaign.Campaign -import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview -import lwacharactersheet.composeapp.generated.resources.Res -import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character -import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc -import org.jetbrains.compose.resources.getString - -class GMCharacterFactory { - - companion object { - const val PLAYER_ID = "PLAYER" - const val NPC_ID = "NPC" - } - - suspend fun convertToGMCharacterPreviewUio( - campaign: Campaign, - characters: List, - filter: String, - tags: Map, - ): List { - val normalizedFilter = filter.unAccent() - - return characters.mapNotNull { - convertToGMCharacterPreviewUio( - campaign = campaign, - character = it, - filter = normalizedFilter, - tags = tags, - ) - } - } - - private suspend fun convertToGMCharacterPreviewUio( - campaign: Campaign, - character: CharacterSheetPreview, - filter: String, - tags: Map, - ): GMCharacterItemUio? { - // get the characterInstanceId from the player list corresponding to this CharacterSheet if any - val isPlayer = campaign.characters.firstOrNull { - it == character.characterSheetId - } != null - // get all characterInstanceId from the npcs list corresponding to this CharacterSheet if any - val isNpc = campaign.npcs.firstOrNull { - it == character.characterSheetId - } != null - // Filter process : Name. - if (filter.isNotEmpty()) { - val normalizedName = character.name.unAccent() - // If the filter is not empty and the character is not - val nameHighlight = normalizedName.contains(other = filter, ignoreCase = true) - if (nameHighlight.not()) { - return null - } - } - // Tag filter process : Player. - if (tags[PLAYER_ID] == true && isPlayer.not()) { - return null - } - // Tag filter process : Npc. - if (tags[NPC_ID] == true && isNpc.not()) { - return null - } - // Build the call tag list. - val previewTagsList = buildList { - if (isPlayer) { - add( - GMTagItemUio( - id = PLAYER_ID, - label = getString(Res.string.game_master__character_tag__character), - highlight = tags[PLAYER_ID] ?: false, - ) - ) - } - if (isNpc) { - add( - GMTagItemUio( - id = NPC_ID, - label = getString(Res.string.game_master__character_tag__npc), - highlight = tags[NPC_ID] ?: false, - ) - ) - } - } - // build the cell action list - val actions = buildList { - add(Action.DisplayPortrait) - when { - isPlayer -> add(Action.RemoveFromGroup) - isNpc -> add(Action.RemoveFromNpc) - else -> { - add(Action.AddToGroup) - add(Action.AddToNpc) - } - } - } - // return the cell UIO. - return GMCharacterItemUio( - characterSheetId = character.characterSheetId, - name = character.name, - level = character.level, - tags = previewTagsList, - actions = actions, - ) - } - - suspend fun convertToGMTagItemUio( - id: String?, - highlight: Boolean, - ): GMTagItemUio? { - return when (id) { - PLAYER_ID -> GMTagItemUio( - id = id, - label = getString(Res.string.game_master__character_tag__character), - highlight = highlight, - ) - - NPC_ID -> GMTagItemUio( - id = id, - label = getString(Res.string.game_master__character_tag__npc), - highlight = highlight, - ) - - else -> null - } - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacter.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacter.kt similarity index 95% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacter.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacter.kt index 1883482..ea79403 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMCharacter.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacter.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.items +package com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.PointerMatcher @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.onClick import androidx.compose.material.DropdownMenu @@ -28,9 +27,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.input.pointer.PointerButton -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio.Action +import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list.GMCharacterItemUio.Action +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__character_action__add_to_group @@ -54,7 +54,7 @@ data class GMCharacterItemUio( val characterSheetId: String, val name: String, val level: Int, - val tags: List, + val tags: List, val actions: List, ) { @Stable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterFactory.kt new file mode 100644 index 0000000..8b8863d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterFactory.kt @@ -0,0 +1,114 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list + +import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list.GMCharacterItemUio.Action +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagUio +import com.pixelized.desktop.lwa.utils.extention.unAccent +import com.pixelized.shared.lwa.model.campaign.Campaign +import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character +import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc +import org.jetbrains.compose.resources.getString + +class GMCharacterFactory { + + companion object { + const val PLAYER_ID = "PLAYER" + const val NPC_ID = "NPC" + } + + fun filterCharacter( + campaign: Campaign, + characters: List, + unAccentFilter: String, + selectedTagId: String?, + ): List { + return characters.filter { + val matchName = it.name.unAccent().contains( + other = unAccentFilter, + ignoreCase = true, + ) + val matchTag = when (selectedTagId) { + null -> true + PLAYER_ID -> campaign.characters.contains(it.characterSheetId) + NPC_ID -> campaign.npcs.contains(it.characterSheetId) + else -> false + } + matchName && matchTag + } + } + + suspend fun convertToGMCharacterPreviewUio( + campaign: Campaign, + characters: List, + tagIdMap: List, + ): List { + return characters.map { + convertToGMCharacterPreviewUio( + campaign = campaign, + character = it, + tagIdMap = tagIdMap, + ) + } + } + + private suspend fun convertToGMCharacterPreviewUio( + campaign: Campaign, + character: CharacterSheetPreview, + tagIdMap: List, + ): GMCharacterItemUio { + val isPlayer = campaign.characters.contains(character.characterSheetId) + val isNpc = campaign.npcs.contains(character.characterSheetId) + + // Build the call tag list. + val tags = tagIdMap.filter { + when (it.id) { + PLAYER_ID -> isPlayer + NPC_ID -> isNpc + else -> false + } + } + + // build the cell action list + val actions = buildList { + add(Action.DisplayPortrait) + when { + isPlayer -> add(Action.RemoveFromGroup) + isNpc -> add(Action.RemoveFromNpc) + else -> { + add(Action.AddToGroup) + add(Action.AddToNpc) + } + } + } + // return the cell UIO. + return GMCharacterItemUio( + characterSheetId = character.characterSheetId, + name = character.name, + level = character.level, + tags = tags, + actions = actions, + ) + } + + suspend fun convertToGMTagItemUio( + id: String?, + highlight: Boolean, + ): GMTagUio? { + return when (id) { + PLAYER_ID -> GMTagUio( + id = id, + label = getString(Res.string.game_master__character_tag__character), + highlight = highlight, + ) + + NPC_ID -> GMTagUio( + id = id, + label = getString(Res.string.game_master__character_tag__npc), + highlight = highlight, + ) + + else -> null + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt similarity index 96% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterPage.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt index 98ec22b..33a75be 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterPage.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterPage.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.character +package com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -44,10 +44,8 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetai import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialog -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacter -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMFilterHeader -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio +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 @@ -163,7 +161,7 @@ fun GMCharacterContent( padding: Dp = 8.dp, spacing: Dp = 8.dp, filter: LwaTextFieldUio, - tags: State>, + tags: State>, characters: State>, onTag: (String) -> Unit, onCharacterAction: (String, GMCharacterItemUio.Action) -> Unit, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterViewModel.kt similarity index 68% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterViewModel.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterViewModel.kt index edd8017..7e913d2 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/GMCharacterViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/character/list/GMCharacterViewModel.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.character +package com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -6,20 +6,20 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio.Action -import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio +import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.list.GMCharacterItemUio.Action +import com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag.GMTagFactory +import com.pixelized.desktop.lwa.utils.extention.unAccent import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.game_master__character__filter -import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character -import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc import org.jetbrains.compose.resources.getString class GMCharacterViewModel( @@ -27,41 +27,53 @@ class GMCharacterViewModel( private val campaignRepository: CampaignRepository, characterSheetRepository: CharacterSheetRepository, private val factory: GMCharacterFactory, + private val tagFactory: GMTagFactory, ) : ViewModel() { - private val _filter = MutableStateFlow("") + private val selectedTagId = MutableStateFlow(null) + private val filterValue = MutableStateFlow("") + val filter = LwaTextFieldUio( enable = true, labelFlow = MutableStateFlow(runBlocking { getString(Res.string.game_master__character__filter) }), - valueFlow = _filter, + valueFlow = filterValue, isError = MutableStateFlow(false), placeHolderFlow = MutableStateFlow(null), - onValueChange = { _filter.value = it }, + onValueChange = { filterValue.value = it }, ) - private val _tags = MutableStateFlow( - mapOf( - GMCharacterFactory.PLAYER_ID to false, - GMCharacterFactory.NPC_ID to false + val tags = combine( + characterSheetRepository.tagsFlow, + selectedTagId, + ) { tags, selectedTagId -> + tagFactory.convertToGMTagItemUio( + tags = tags.values, + selectedTagId = selectedTagId, ) - ) - val tags = _tags.map { it: Map -> - it.mapNotNull { (id, highlight) -> - factory.convertToGMTagItemUio(id = id, highlight = highlight) - } }.stateIn( scope = viewModelScope, - started = SharingStarted.Eagerly, + started = SharingStarted.Lazily, initialValue = emptyList(), ) val characters = combine( campaignRepository.campaignFlow, characterSheetRepository.characterSheetPreviewFlow, - filter.valueFlow, - _tags, - factory::convertToGMCharacterPreviewUio, - ).stateIn( + filter.valueFlow.map { it.unAccent() }, + tags, + selectedTagId, + ) { campaign, characters, unAccentFilter, tagIdMap, selectedTagId -> + factory.convertToGMCharacterPreviewUio( + characters = factory.filterCharacter( + campaign = campaign, + characters = characters, + unAccentFilter = unAccentFilter, + selectedTagId = selectedTagId, + ), + campaign = campaign, + tagIdMap = tagIdMap, + ) + }.stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = emptyList(), @@ -106,8 +118,11 @@ class GMCharacterViewModel( fun onTag( id: String, ) { - _tags.value = _tags.value.toMutableMap().also { - it[id] = it.getOrPut(id) { true }.not() + selectedTagId.update { + when (it) { + id -> null + else -> id + } } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMFilterHeader.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/GMFilterHeader.kt similarity index 68% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMFilterHeader.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/GMFilterHeader.kt index f5b7e9f..1f00d37 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMFilterHeader.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/GMFilterHeader.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.items +package com.pixelized.desktop.lwa.ui.screen.gamemaster.common import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -28,6 +28,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +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 kotlinx.coroutines.launch import lwacharactersheet.composeapp.generated.resources.Res @@ -41,7 +43,7 @@ fun GMFilterHeader( padding: Dp = 16.dp, spacing: Dp = 8.dp, filter: LwaTextFieldUio, - tags: State>, + tags: State>, onTag: (String) -> Unit, ) { val scope = rememberCoroutineScope() @@ -54,7 +56,6 @@ fun GMFilterHeader( field = filter, trailingIcon = { val value = filter.valueFlow.collectAsState() - AnimatedVisibility( visible = value.value.isNotBlank(), enter = fadeIn(), @@ -72,28 +73,37 @@ fun GMFilterHeader( } } ) - LazyRow( - modifier = Modifier.draggable( - orientation = Orientation.Horizontal, - state = rememberDraggableState { delta -> - scope.launch { - lazyListState.scrollBy(-delta) - } - }, - ), - state = lazyListState, - contentPadding = remember(padding, spacing) { - PaddingValues(horizontal = padding, vertical = spacing) - }, - horizontalArrangement = Arrangement.spacedBy(space = spacing), + AnimatedVisibility( + visible = tags.value.isNotEmpty(), + enter = fadeIn(), + exit = fadeOut(), ) { - items( - items = tags.value, - ) { tag -> - GMTag( - tag = tag, - onTag = { onTag(tag.id) }, - ) + LazyRow( + modifier = Modifier.draggable( + orientation = Orientation.Horizontal, + state = rememberDraggableState { delta -> + scope.launch { + lazyListState.scrollBy(-delta) + } + }, + ), + state = lazyListState, + contentPadding = remember(padding, spacing) { + PaddingValues( + horizontal = padding, + vertical = spacing, + ) + }, + horizontalArrangement = Arrangement.spacedBy(space = spacing), + ) { + items( + items = tags.value, + ) { tag -> + GMTag( + tag = tag, + onTag = { onTag(tag.id) }, + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTab.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/GMTab.kt similarity index 95% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTab.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/GMTab.kt index 1ef66f9..00a5f81 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTab.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/GMTab.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.items +package com.pixelized.desktop.lwa.ui.screen.gamemaster.common import androidx.compose.material.Icon import androidx.compose.material.IconButton diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTagItemUio.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt similarity index 94% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTagItemUio.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt index 5666c4f..1437e22 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/items/GMTagItemUio.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTag.kt @@ -1,4 +1,4 @@ -package com.pixelized.desktop.lwa.ui.screen.gamemaster.items +package com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.clickable @@ -19,7 +19,7 @@ import com.pixelized.desktop.lwa.ui.theme.lwa @Stable -data class GMTagItemUio( +data class GMTagUio( val id: String, val label: String, val highlight: Boolean, @@ -36,7 +36,7 @@ fun GMTag( padding: PaddingValues = GmTagDefault.padding, shape: Shape = CircleShape, elevation: Dp = 2.dp, - tag: GMTagItemUio, + tag: GMTagUio, onTag: (() -> Unit)? = null, ) { val animatedColor = animateColorAsState( diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTagFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTagFactory.kt new file mode 100644 index 0000000..55167f9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/gamemaster/common/tag/GMTagFactory.kt @@ -0,0 +1,29 @@ +package com.pixelized.desktop.lwa.ui.screen.gamemaster.common.tag + +import com.pixelized.shared.lwa.model.tag.Tag + +class GMTagFactory { + + fun convertToGMTagItemUio( + tags: Collection, + selectedTagId: String?, + ): List { + return tags.map { tag -> + convertToGMTagItemUio( + tag = tag, + selectedTagId = selectedTagId, + ) + } + } + + fun convertToGMTagItemUio( + tag: Tag, + selectedTagId: String?, + ): GMTagUio { + return GMTagUio( + id = tag.id, + label = tag.label, + highlight = tag.id == selectedTagId, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillNormalizerUseCaseText.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillUioNormalizerUseCaseText.kt similarity index 98% rename from composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillNormalizerUseCaseText.kt rename to composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillUioNormalizerUseCaseText.kt index e90dedf..f1ded63 100644 --- a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillNormalizerUseCaseText.kt +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillUioNormalizerUseCaseText.kt @@ -3,7 +3,7 @@ package com.pixelized.desktop.lwa.business import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase import org.junit.Test -class SkillNormalizerUseCaseText { +class SkillUioNormalizerUseCaseText { @Test fun testNormalization() { diff --git a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCaseTest.kt b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillUioStepUseCaseTest.kt similarity index 99% rename from composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCaseTest.kt rename to composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillUioStepUseCaseTest.kt index 24d7bed..48b8d44 100644 --- a/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillStepUseCaseTest.kt +++ b/composeApp/src/commonTest/kotlin/com/pixelized/desktop/lwa/business/SkillUioStepUseCaseTest.kt @@ -4,7 +4,7 @@ import com.pixelized.shared.lwa.usecase.SkillStepUseCase import com.pixelized.shared.lwa.usecase.SkillStepUseCase.SkillStep import org.junit.Test -class SkillStepUseCaseTest { +class SkillUioStepUseCaseTest { @Test fun testStepForSkill() { diff --git a/server/src/main/kotlin/Module.kt b/server/src/main/kotlin/Module.kt index 8ba731c..a17215f 100644 --- a/server/src/main/kotlin/Module.kt +++ b/server/src/main/kotlin/Module.kt @@ -4,6 +4,7 @@ import com.pixelized.server.lwa.model.campaign.CampaignService import com.pixelized.server.lwa.model.campaign.CampaignStore import com.pixelized.server.lwa.model.character.CharacterSheetService import com.pixelized.server.lwa.model.character.CharacterSheetStore +import com.pixelized.server.lwa.model.tag.TagStore import com.pixelized.server.lwa.server.Engine import com.pixelized.shared.lwa.utils.PathProvider import org.koin.core.module.dsl.createdAtStart @@ -35,6 +36,7 @@ val storeDependencies singleOf(::CharacterSheetStore) singleOf(::CampaignStore) singleOf(::AlterationStore) + singleOf(::TagStore) } val serviceDependencies diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt index f0fda7a..9d5b098 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt @@ -1,6 +1,9 @@ package com.pixelized.server.lwa.model.alteration +import com.pixelized.server.lwa.model.tag.TagStore import com.pixelized.shared.lwa.model.alteration.AlterationJson +import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory +import com.pixelized.shared.lwa.model.tag.TagJson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -9,25 +12,45 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn class AlterationService( - store: AlterationStore, + private val alterationStore: AlterationStore, + tagStore: TagStore, + factory: AlterationJsonFactory, ) { private val scope = CoroutineScope(Dispatchers.IO + Job()) - private val alterationsFlow = store.alterationsFlow() - - private val alterationHashFlow = alterationsFlow - .map { data -> data.associateBy { it.id } } + private val alterationHashFlow = alterationStore.alterationsFlow() + .map { alterations -> alterations.associate { it.id to factory.convertToJson(it) } } .stateIn( scope = scope, started = SharingStarted.Eagerly, initialValue = emptyMap() ) + private val alterationTags = tagStore.alterationTags() + .map { it.values.toList() } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + fun alterations(): List { - return alterationsFlow.value + return alterationHashFlow.value.values.toList() + } + + fun tags(): List { + return alterationTags.value } fun alteration(alterationId: String): AlterationJson? { return alterationHashFlow.value[alterationId] } + + fun update(json: AlterationJson) { + alterationStore.save(alteration = json) + } + + fun delete(alterationId: String): Boolean { + return alterationStore.delete(id = alterationId) + } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt index cee2d82..6505b33 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt @@ -1,22 +1,27 @@ package com.pixelized.server.lwa.model.alteration +import com.pixelized.shared.lwa.model.alteration.Alteration import com.pixelized.shared.lwa.model.alteration.AlterationJson +import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory import com.pixelized.shared.lwa.utils.PathProvider 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 import kotlinx.serialization.json.Json import java.io.File class AlterationStore( private val pathProvider: PathProvider, + private val factory: AlterationJsonFactory, private val json: Json, ) { private val directory = File(pathProvider.alterationsPath()).also { it.mkdirs() } - private val alterationsFlow = MutableStateFlow>(emptyList()) + + private val alterationFlow = MutableStateFlow>(emptyList()) init { // build a coroutine scope for async calls @@ -27,10 +32,10 @@ class AlterationStore( } } - fun alterationsFlow(): StateFlow> = alterationsFlow + fun alterationsFlow(): StateFlow> = alterationFlow private fun updateAlterations() { - alterationsFlow.value = try { + alterationFlow.value = try { loadAlterations() } catch (exception: Exception) { println(exception) // TODO proper exception handling @@ -42,7 +47,7 @@ class AlterationStore( FileReadException::class, JsonConversionException::class, ) - private fun loadAlterations(): List { + private fun loadAlterations(): List { return directory .listFiles() ?.mapNotNull { file -> @@ -56,7 +61,8 @@ class AlterationStore( return@mapNotNull null } try { - this.json.decodeFromString(json) + val data = this.json.decodeFromString(json) + factory.convertFromJson(data) } catch (exception: Exception) { throw JsonConversionException(root = exception) } @@ -64,6 +70,73 @@ class AlterationStore( ?: emptyList() } + @Throws(JsonConversionException::class, FileWriteException::class) + fun save( + alteration: Alteration, + ) { + val json = try { + factory.convertToJson(data = alteration) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + + save(alteration = json) + } + + @Throws(FileWriteException::class) + fun save( + alteration: AlterationJson, + ) { + // encode the json into a string + val data = try { + json.encodeToString(alteration) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + // write the alteration into a file. + try { + val file = alterationFile(id = alteration.id) + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException( + root = exception + ) + } + // Update the dataflow. + alterationFlow.update { alterations -> + val index = alterations.indexOfFirst { it.id == alteration.id } + val alt = factory.convertFromJson(alteration) + + alterations.toMutableList().also { + if (index >= 0) { + it[index] = alt + } else { + it.add(alt) + } + } + } + } + + fun delete(id: String): Boolean { + val file = alterationFile(id = id) + val deleted = file.delete() + if (deleted) { + alterationFlow.update { alterations -> + alterations.toMutableList().also { alteration -> + alteration.removeIf { it.id == id } + } + } + } + return deleted + } + + private fun alterationFile(id: String): File { + return File("${pathProvider.alterationsPath()}${id}.json") + } + sealed class AlterationStoreException(root: Exception) : Exception(root) class JsonConversionException(root: Exception) : AlterationStoreException(root) class FileWriteException(root: Exception) : AlterationStoreException(root) diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt index 9187c47..5222b7d 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt @@ -1,8 +1,10 @@ package com.pixelized.server.lwa.model.character +import com.pixelized.server.lwa.model.tag.TagStore import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson import com.pixelized.shared.lwa.model.characterSheet.factory.CharacterSheetJsonFactory +import com.pixelized.shared.lwa.model.tag.TagJson import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -12,12 +14,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn class CharacterSheetService( - private val store: CharacterSheetStore, + private val characterStore: CharacterSheetStore, + private val tagStore: TagStore, private val factory: CharacterSheetJsonFactory, ) { private val scope = CoroutineScope(Dispatchers.IO + Job()) private val sheets get() = sheetsFlow.value - private val sheetsFlow = store.characterSheetsFlow() + private val sheetsFlow = characterStore.characterSheetsFlow() .map { entry -> entry.associateBy { character -> character.id } } .stateIn( scope = scope, @@ -25,10 +28,22 @@ class CharacterSheetService( initialValue = emptyMap() ) + private val alterationTags = tagStore.characterTags() + .map { it.values.toList() } + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + fun character(characterSheetId: String): CharacterSheet? { return sheets[characterSheetId] } + fun tags() : Collection { + return alterationTags.value + } + fun charactersJson(): List { return sheets.map { factory.convertToPreviewJson(sheet = it.value) } } @@ -38,13 +53,13 @@ class CharacterSheetService( } suspend fun updateCharacterSheet(character: CharacterSheetJson) { - return store.save( + return characterStore.save( sheet = factory.convertFromJson(character) ) } fun deleteCharacterSheet(characterSheetId: String): Boolean { - return store.delete(id = characterSheetId) + return characterStore.delete(id = characterSheetId) } // Data manipulation through WebSocket. @@ -60,7 +75,7 @@ class CharacterSheetService( val alterations = character.alterations.toMutableList().also { it.add(alterationId) } - store.save( + characterStore.save( sheet = character.copy( alterations = alterations, ) @@ -70,7 +85,7 @@ class CharacterSheetService( val alterations = character.alterations.toMutableList().also { it.remove(alterationId) } - store.save( + characterStore.save( sheet = character.copy( alterations = alterations, ) @@ -85,7 +100,7 @@ class CharacterSheetService( ) { sheets[characterSheetId]?.let { character -> val update = character.copy(damage = damage) - store.save(sheet = update) + characterStore.save(sheet = update) } } @@ -95,7 +110,7 @@ class CharacterSheetService( ) { sheets[characterSheetId]?.let { character -> val update = character.copy(diminished = diminished) - store.save(sheet = update) + characterStore.save(sheet = update) } } @@ -105,7 +120,7 @@ class CharacterSheetService( ) { sheets[characterSheetId]?.let { character -> val update = character.copy(fatigue = fatigue) - store.save(sheet = update) + characterStore.save(sheet = update) } } @@ -126,7 +141,7 @@ class CharacterSheetService( skill.takeIf { skill.id == skillId }?.copy(used = used) ?: skill }, ) - store.save(sheet = update) + characterStore.save(sheet = update) } } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt index 0b611e7..2a7e54b 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt @@ -76,7 +76,7 @@ class CharacterSheetStore( ) fun save(sheet: CharacterSheet) { // convert the character sheet into json format. - val json = try { + val data = try { factory.convertToJson(sheet = sheet).let(json::encodeToString) } catch (exception: Exception) { throw JsonConversionException(root = exception) @@ -85,7 +85,7 @@ class CharacterSheetStore( try { val file = characterSheetFile(id = sheet.id) file.writeText( - text = json, + text = data, charset = Charsets.UTF_8, ) } catch (exception: Exception) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt new file mode 100644 index 0000000..137e4d0 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt @@ -0,0 +1,108 @@ +package com.pixelized.server.lwa.model.tag + +import com.pixelized.shared.lwa.model.tag.TagJson +import com.pixelized.shared.lwa.utils.PathProvider +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.launch +import kotlinx.serialization.json.Json +import java.io.File + +private const val CHARACTER = "character" +private const val ALTERATION = "alteration" + +class TagStore( + private val pathProvider: PathProvider, + private val json: Json, +) { + private val alterationTagsFlow = MutableStateFlow>(emptyMap()) + private val characterTagsFlow = MutableStateFlow>(emptyMap()) + + init { + // make the file path. + File(pathProvider.tagsPath()).mkdirs() + // build a coroutine scope for async calls + val scope = CoroutineScope(Dispatchers.IO + Job()) + // load the initial data + scope.launch { + update( + flow = alterationTagsFlow, + file = alterationFile(), + ) + update( + flow = characterTagsFlow, + file = characterFile(), + ) + } + } + + fun alterationTags(): StateFlow> = alterationTagsFlow + fun characterTags(): StateFlow> = characterTagsFlow + + private fun update( + flow: MutableStateFlow>, + file: File, + ) { + flow.value = try { + file.readTags().associateBy { it.id } + } catch (exception: Exception) { + println(exception) // TODO proper exception handling + emptyMap() + } + } + + @Throws(FileReadException::class, JsonConversionException::class) + private fun File.readTags(): List { + // read the file (force the UTF8 format) + val data = try { + readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the json is blank no alteration have been save, ignore this file. + if (data.isBlank()) { + return emptyList() + } + return try { + json.decodeFromString>(data) + } catch (exception: Exception) { + throw JsonConversionException( + root = exception + ) + } + } + + @Throws(JsonConversionException::class, FileWriteException::class) + private fun saveAlterationTags(tags: List) { + // convert the data to json format + val json = try { + this.json.encodeToString(tags) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + // write the file + try { + val file = alterationFile() + file.writeText( + text = json, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + alterationTagsFlow.value = tags.associateBy { it.id } + } + + private fun characterFile() = File("${pathProvider.tagsPath()}$CHARACTER.json") + + private fun alterationFile() = File("${pathProvider.tagsPath()}$ALTERATION.json") + + sealed class TagStoreException(root: Exception) : Exception(root) + class JsonConversionException(root: Exception) : TagStoreException(root) + class FileWriteException(root: Exception) : TagStoreException(root) + class FileReadException(root: Exception) : TagStoreException(root) +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt index 8a3d1b5..49cfc50 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt @@ -1,8 +1,11 @@ package com.pixelized.server.lwa.server +import com.pixelized.server.lwa.server.rest.alteration.deleteAlteration import com.pixelized.server.lwa.server.rest.alteration.getAlteration +import com.pixelized.server.lwa.server.rest.alteration.getAlterationTags import com.pixelized.server.lwa.server.rest.alteration.getAlterations +import com.pixelized.server.lwa.server.rest.alteration.putAlteration import com.pixelized.server.lwa.server.rest.campaign.removeCampaignNpc import com.pixelized.server.lwa.server.rest.campaign.getCampaign import com.pixelized.server.lwa.server.rest.campaign.putCampaignCharacter @@ -11,6 +14,7 @@ import com.pixelized.server.lwa.server.rest.campaign.putCampaignScene import com.pixelized.server.lwa.server.rest.campaign.removeCampaignCharacter import com.pixelized.server.lwa.server.rest.character.deleteCharacter import com.pixelized.server.lwa.server.rest.character.getCharacter +import com.pixelized.server.lwa.server.rest.character.getCharacterTags import com.pixelized.server.lwa.server.rest.character.getCharacters import com.pixelized.server.lwa.server.rest.character.putCharacter import com.pixelized.server.lwa.server.rest.character.putCharacterAlteration @@ -126,6 +130,18 @@ class LocalServer { path = "/detail", body = engine.getAlteration(), ) + get( + path = "/tags", + body = engine.getAlterationTags(), + ) + put( + path = "/update", + body = engine.putAlteration() + ) + delete( + path = "/delete", + body = engine.deleteAlteration() + ) } route( path = "/character", @@ -134,6 +150,10 @@ class LocalServer { path = "/all", body = engine.getCharacters(), ) + get( + path = "/tags", + body = engine.getCharacterTags(), + ) get( path = "/detail", body = engine.getCharacter(), diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/DELETE_Alteration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/DELETE_Alteration.kt new file mode 100644 index 0000000..c347afc --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/DELETE_Alteration.kt @@ -0,0 +1,37 @@ +package com.pixelized.server.lwa.server.rest.alteration + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.server.lwa.utils.extentions.alterationId +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import io.ktor.http.HttpStatusCode +import io.ktor.server.response.respondText + +fun Engine.deleteAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { + return { + try { + val alterationId = call.parameters.alterationId + + val deleted = alterationService.delete( + alterationId = alterationId + ) + + if (deleted.not()) error("Unexpected error occurred") + + call.respondText( + text = "${HttpStatusCode.OK}", + status = HttpStatusCode.OK, + ) + webSocket.emit( + value = ApiSynchronisation.AlterationDelete( + timestamp = System.currentTimeMillis(), + alterationId = alterationId, + ), + ) + } catch (exception: Exception) { + call.respondText( + text = "${HttpStatusCode.UnprocessableEntity}", + status = HttpStatusCode.UnprocessableEntity, + ) + } + } +} diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_AlterationTags.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_AlterationTags.kt new file mode 100644 index 0000000..23baeeb --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/GET_AlterationTags.kt @@ -0,0 +1,12 @@ +package com.pixelized.server.lwa.server.rest.alteration + +import com.pixelized.server.lwa.server.Engine +import io.ktor.server.response.respond + +fun Engine.getAlterationTags(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { + return { + call.respond( + message = alterationService.tags(), + ) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_Alateration.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_Alateration.kt new file mode 100644 index 0000000..6885a81 --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/alteration/PUT_Alateration.kt @@ -0,0 +1,34 @@ +package com.pixelized.server.lwa.server.rest.alteration + +import com.pixelized.server.lwa.server.Engine +import com.pixelized.shared.lwa.model.alteration.AlterationJson +import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation +import io.ktor.http.HttpStatusCode +import io.ktor.server.request.receive +import io.ktor.server.response.respondText + +fun Engine.putAlteration(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { + return { + try { + val form = call.receive() + + alterationService.update(json = form) + + call.respondText( + text = "${HttpStatusCode.OK}", + status = HttpStatusCode.OK, + ) + webSocket.emit( + value = ApiSynchronisation.AlterationUpdate( + timestamp = System.currentTimeMillis(), + alterationId = form.id, + ), + ) + } catch (exception : Exception) { + call.respondText( + text = "${HttpStatusCode.UnprocessableEntity}", + status = HttpStatusCode.UnprocessableEntity, + ) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt index a9e7ab7..28dea04 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/DELETE_Character.kt @@ -14,6 +14,7 @@ fun Engine.deleteCharacter(): suspend io.ktor.server.routing.RoutingContext.() - characterSheetId = characterSheetId ) // Remove the character fom the campaign if needed. + // TODO probably useless because all data will not be cleaned up (all campaign / screnes) campaignService.removeInstance( characterSheetId = characterSheetId, ) diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_CharacterTags.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_CharacterTags.kt new file mode 100644 index 0000000..9da14ff --- /dev/null +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/character/GET_CharacterTags.kt @@ -0,0 +1,12 @@ +package com.pixelized.server.lwa.server.rest.character + +import com.pixelized.server.lwa.server.Engine +import io.ktor.server.response.respond + +fun Engine.getCharacterTags(): suspend io.ktor.server.routing.RoutingContext.() -> Unit { + return { + call.respond( + message = characterService.tags(), + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt index e4203b0..a7a9956 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/Module.kt @@ -7,6 +7,7 @@ import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonV1Factory import com.pixelized.shared.lwa.model.campaign.factory.CampaignJsonV2Factory import com.pixelized.shared.lwa.model.characterSheet.factory.CharacterSheetJsonFactory import com.pixelized.shared.lwa.model.characterSheet.factory.CharacterSheetJsonV1Factory +import com.pixelized.shared.lwa.model.tag.TagJsonFactory import com.pixelized.shared.lwa.parser.dice.DiceParser import com.pixelized.shared.lwa.parser.expression.ExpressionParser import com.pixelized.shared.lwa.parser.word.WordParser @@ -45,6 +46,7 @@ val factoryDependencies factoryOf(::CampaignJsonV2Factory) factoryOf(::AlteredCharacterSheetFactory) factoryOf(::AlterationJsonFactory) + factoryOf(::TagJsonFactory) } val parserDependencies diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/Tag.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/Tag.kt new file mode 100644 index 0000000..545ad9f --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/Tag.kt @@ -0,0 +1,6 @@ +package com.pixelized.shared.lwa.model.tag + +data class Tag( + val id: String, + val label: String, +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJson.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJson.kt new file mode 100644 index 0000000..f924f84 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJson.kt @@ -0,0 +1,8 @@ +package com.pixelized.shared.lwa.model.tag + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface TagJson { + val id: String +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonFactory.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonFactory.kt new file mode 100644 index 0000000..874d30c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonFactory.kt @@ -0,0 +1,24 @@ +package com.pixelized.shared.lwa.model.tag + +class TagJsonFactory { + + fun convertFromJson( + json: TagJson, + ): Tag { + return when (json) { + is TagJsonV1 -> Tag( + id = json.id, + label = json.label, + ) + } + } + + fun convertToJson( + tag: Tag, + ): TagJson { + return TagJsonV1( + id = tag.id, + label = tag.label, + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonV1.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonV1.kt new file mode 100644 index 0000000..500456a --- /dev/null +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/tag/TagJsonV1.kt @@ -0,0 +1,9 @@ +package com.pixelized.shared.lwa.model.tag + +import kotlinx.serialization.Serializable + +@Serializable +data class TagJsonV1( + override val id: String, + val label: String, +) : TagJson \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt index 6fecdd1..1394be5 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/websocket/ApiSynchronisation.kt @@ -16,4 +16,16 @@ sealed interface ApiSynchronisation : SocketMessage { override val timestamp: Long, override val characterSheetId: String, ) : ApiSynchronisation, CharacterSheetIdMessage + + @Serializable + data class AlterationUpdate( + override val timestamp: Long, + val alterationId: String, + ) : ApiSynchronisation + + @Serializable + data class AlterationDelete( + override val timestamp: Long, + val alterationId: String, + ) : ApiSynchronisation } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt index a400d12..80e5c12 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/utils/PathProvider.kt @@ -53,4 +53,14 @@ class PathProvider( OperatingSystem.Macintosh -> "${storePath(os = os, app = app)}alterations/" } } + + fun tagsPath( + os: OperatingSystem = this.operatingSystem, + app: String = this.appName, + ): String { + return when (os) { + OperatingSystem.Windows -> "${storePath(os = os, app = app)}tags\\" + OperatingSystem.Macintosh -> "${storePath(os = os, app = app)}tags/" + } + } }