diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 393fff4..63ef00e 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -316,8 +316,8 @@ Identifiant de l'objet Nom Description - Image url - Vignette url + Url de l'image + Url de la vignette Empilable Équipable Consommable @@ -337,4 +337,19 @@ Montrer le groupe de npcs Montrer le panneau latéral droit pour tous les joueurs. + Identifiant de la carte + Nom de la carte + Camera + Zoom + X + Y + Taille + Largeur + Hauteur + Carte + Region + Identifiant de la couche + Nom de la couche + Url de l'image + \ No newline at end of file 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 937d51c..98bf693 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/Module.kt @@ -37,19 +37,6 @@ import com.pixelized.desktop.lwa.ui.composable.character.purse.PurseDialogViewMo import com.pixelized.desktop.lwa.ui.composable.image.ImagerModelConverter import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlayViewModel import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory -import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryFactory -import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetFactory -import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory -import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.NpcRibbonViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.CampaignChatViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.TextMessageFactory -import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.links.ResourcesViewModel -import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkFactory -import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkViewModel import com.pixelized.desktop.lwa.ui.screen.admin.AdminViewModel import com.pixelized.desktop.lwa.ui.screen.admin.action.GMActionUseCase import com.pixelized.desktop.lwa.ui.screen.admin.action.GMActionViewModel @@ -68,8 +55,25 @@ import com.pixelized.desktop.lwa.ui.screen.admin.item.edit.GMItemEditFactory import com.pixelized.desktop.lwa.ui.screen.admin.item.edit.GMItemEditViewModel import com.pixelized.desktop.lwa.ui.screen.admin.item.list.GMItemFactory import com.pixelized.desktop.lwa.ui.screen.admin.item.list.GMItemViewModel +import com.pixelized.desktop.lwa.ui.screen.admin.map.edit.GMMapEditFactory +import com.pixelized.desktop.lwa.ui.screen.admin.map.edit.GMMapEditViewModel +import com.pixelized.desktop.lwa.ui.screen.admin.map.list.GMMapFactory +import com.pixelized.desktop.lwa.ui.screen.admin.map.list.GMMapViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.CampaignChatViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.TextMessageFactory import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapSceneFactory import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapSceneViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory +import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryFactory +import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.CharacterDetailSheetFactory +import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory +import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.NpcRibbonViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.links.ResourcesViewModel +import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkFactory +import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkViewModel 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.settings.SettingsViewModel @@ -174,6 +178,8 @@ val factoryDependencies factoryOf(::GMAlterationEditFactory) factoryOf(::GMItemFactory) factoryOf(::GMItemEditFactory) + factoryOf(::GMMapFactory) + factoryOf(::GMMapEditFactory) } val viewModelDependencies @@ -206,6 +212,8 @@ val viewModelDependencies viewModelOf(::GMAlterationEditViewModel) viewModelOf(::GMItemViewModel) viewModelOf(::GMItemEditViewModel) + viewModelOf(::GMMapViewModel) + viewModelOf(::GMMapEditViewModel) } 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 c7952c0..063c375 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 @@ -177,6 +177,10 @@ interface LwaClient { create: Boolean, ): APIResponse + suspend fun deleteMap( + mapId: String, + ): APIResponse + companion object { fun error(error: APIResponse<*>): Nothing = throw LwaNetworkException(error) } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/network/LwaClientImpl.kt index 991805b..018265e 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 @@ -299,9 +299,16 @@ class LwaClientImpl( mapJson: MapJson, create: Boolean, ): APIResponse = client - .put("$root/map/update") { + .put("$root/map/update?create=$create") { contentType(ContentType.Application.Json) setBody(mapJson) } .body>() + + @Throws + override suspend fun deleteMap( + mapId: String, + ): APIResponse = client + .delete("$root/map/delete?mapId=$mapId") + .body>() } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapRepository.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapRepository.kt index 60a9c2d..4955581 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapRepository.kt @@ -35,4 +35,11 @@ class MapRepository( create = create, ) } + + @Throws + suspend fun deleteMap( + mapId: String, + ) { + mapStore.deleteMap(mapId = mapId) + } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapStore.kt index 8313659..355645d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/map/MapStore.kt @@ -80,6 +80,18 @@ class MapStore( } } + @Throws + suspend fun deleteMap( + mapId: String, + ) { + val request = client.deleteMap( + mapId = mapId, + ) + if (request.success.not()) { + LwaClient.error(error = request) + } + } + // region: WebSocket & data update. private suspend fun handleMessage(message: SocketMessage) { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagStore.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagStore.kt index 0cb0ca3..61a0faf 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagStore.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/repository/tag/TagStore.kt @@ -15,6 +15,7 @@ class TagStore( private val characterTagsFlow = MutableStateFlow>(emptyMap()) private val alterationTagsFlow = MutableStateFlow>(emptyMap()) private val itemTagsFlow = MutableStateFlow>(emptyMap()) + private val mapTagsFlow = MutableStateFlow>(emptyMap()) fun charactersTagFlow(): StateFlow> = characterTagsFlow diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemEditDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemEditDestination.kt index d6cf94a..309dac8 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemEditDestination.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMItemEditDestination.kt @@ -12,7 +12,7 @@ import com.pixelized.desktop.lwa.utils.extention.ARG @Stable object GMItemEditDestination { - private const val ROUTE = "GameMasterItem" + private const val ROUTE = "GameMasterItemEdit" private const val ITEM_ID = "id" @Stable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMMapDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMMapDestination.kt new file mode 100644 index 0000000..0e947bc --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMMapDestination.kt @@ -0,0 +1,26 @@ +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.admin.map.list.GMMapPage + +object GMMapDestination { + private const val ROUTE = "GameMasterMap" + + fun baseRoute() = ROUTE + fun navigationRoute() = ROUTE +} + +fun NavGraphBuilder.composableGameMasterMapPage() { + composable( + route = GMMapDestination.baseRoute(), + ) { + GMMapPage() + } +} + +fun NavHostController.navigateToGameMasterMapPage() { + val route = GMMapDestination.navigationRoute() + navigate(route = route) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMMapEditDestination.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMMapEditDestination.kt new file mode 100644 index 0000000..aacc5ac --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/navigation/screen/destination/gamemaster/GMMapEditDestination.kt @@ -0,0 +1,56 @@ +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.admin.map.edit.GMMapEditPage +import com.pixelized.desktop.lwa.ui.screen.admin.map.list.GMMapPage +import com.pixelized.desktop.lwa.utils.extention.ARG + +object GMMapEditDestination { + private const val ROUTE = "GameMasterMapEdit" + private const val ARG_MAP_ID = "mapId" + + fun baseRoute() = "$ROUTE?${ARG_MAP_ID.ARG}" + + fun navigationRoute(mapId: String?) = "$ROUTE?$ARG_MAP_ID=$mapId" + + @Stable + fun arguments() = listOf( + navArgument(ARG_MAP_ID) { + nullable = true + type = NavType.StringType + }, + ) + + @Stable + data class Argument( + val id: String?, + ) { + constructor(savedStateHandle: SavedStateHandle) : this( + id = savedStateHandle.get(ARG_MAP_ID), + ) + } +} + +fun NavGraphBuilder.composableGameMasterMapEditPage() { + composable( + route = GMMapEditDestination.baseRoute(), + arguments = GMMapEditDestination.arguments(), + ) { + GMMapEditPage() + } +} + +fun NavHostController.navigateToGameMasterMapEditPage( + mapId: String?, +) { + val route = GMMapEditDestination.navigationRoute( + mapId = mapId, + ) + navigate(route = route) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/AdminNavHost.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/AdminNavHost.kt index 7956629..b26f62a 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/AdminNavHost.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/AdminNavHost.kt @@ -16,6 +16,7 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.com import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterCharacterEditPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterItemEditPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterMainPage +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterMapEditPage val LocalGMScreenController = compositionLocalOf { error("GameMaster NavHost controller is not yet ready") @@ -38,9 +39,11 @@ fun GameMasterNavHost() { startDestination = GameMasterDestination.navigationRoute(), ) { composableGameMasterMainPage() + composableGameMasterCharacterEditPage() composableGameMasterAlterationEditPage() composableGameMasterItemEditPage() + composableGameMasterMapEditPage() composableLevelUp() } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/AdminScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/AdminScreen.kt index bb69657..425bc5c 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/AdminScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/AdminScreen.kt @@ -37,23 +37,24 @@ import androidx.navigation.compose.rememberNavController import com.pixelized.desktop.lwa.BuildKonfig 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.composableGameMasterMapPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.composableGameMasterObjectPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterActionPage 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.navigateToGameMasterMapPage import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterObjectPage -import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.dice.DiceMenu import com.pixelized.desktop.lwa.ui.screen.admin.common.GMTab import com.pixelized.desktop.lwa.ui.screen.admin.common.GMTabUio +import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.dice.DiceMenu import com.pixelized.desktop.lwa.ui.theme.color.component.LwaSwitchColors import com.pixelized.desktop.lwa.ui.theme.lwa import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.app_version import lwacharactersheet.composeapp.generated.resources.game_master__action import lwacharactersheet.composeapp.generated.resources.game_master__title -import lwacharactersheet.composeapp.generated.resources.app_version import lwacharactersheet.composeapp.generated.resources.icon_d100 import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -79,6 +80,7 @@ fun GameMasterScreen( GMTabUio.Characters -> controller.navigateToGameMasterCharacterPage() GMTabUio.Alterations -> controller.navigateToGameMasterAlterationPage() GMTabUio.Objects -> controller.navigateToGameMasterObjectPage() + GMTabUio.Map -> controller.navigateToGameMasterMapPage() } }, onDice = { @@ -197,8 +199,8 @@ private fun GameMasterContent( composableGameMasterActionPage() composableGameMasterCharacterPage() composableGameMasterAlterationPage() - composableGameMasterAlterationEditPage() composableGameMasterObjectPage() + composableGameMasterMapPage() } } } diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/common/GMTab.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/common/GMTab.kt index 6601f1a..dd9dbb2 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/common/GMTab.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/common/GMTab.kt @@ -10,6 +10,7 @@ import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.ic_account_child_invert_24dp import lwacharactersheet.composeapp.generated.resources.ic_blur_on_24dp import lwacharactersheet.composeapp.generated.resources.ic_iron_24dp +import lwacharactersheet.composeapp.generated.resources.ic_map_24dp import lwacharactersheet.composeapp.generated.resources.ic_special_character_24dp import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.painterResource @@ -22,6 +23,7 @@ enum class GMTabUio( Characters(icon = Res.drawable.ic_account_child_invert_24dp), Alterations(icon = Res.drawable.ic_blur_on_24dp), Objects(icon = Res.drawable.ic_iron_24dp), + Map(icon = Res.drawable.ic_map_24dp), } @Composable diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditFactory.kt new file mode 100644 index 0000000..002fb0b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditFactory.kt @@ -0,0 +1,148 @@ +package com.pixelized.desktop.lwa.ui.screen.admin.map.edit + +import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextField +import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextFieldFlow +import com.pixelized.desktop.lwa.utils.extention.unpack +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_camera_offset_x +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_camera_offset_y +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_camera_zoom +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_id +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_name +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_size_height +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_size_width +import lwacharactersheet.composeapp.generated.resources.game_master__map__layer_id +import lwacharactersheet.composeapp.generated.resources.game_master__map__layer_name +import lwacharactersheet.composeapp.generated.resources.game_master__map__layer_uri +import org.jetbrains.compose.resources.getString +import com.pixelized.shared.lwa.model.map.Map as MiniMap + +class GMMapEditFactory { + + suspend fun createForm( + item: MiniMap?, + ): GMMapEditViewModel.GMMapEditPageForm { + val id = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__edit_id), + value = item?.id ?: "" + ) + val label = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__edit_name), + value = item?.name ?: "" + ) + val cameraZoom = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__edit_camera_zoom), + value = "${item?.camera?.zoom ?: 0}", + ) + val cameraOffsetX = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__edit_camera_offset_x), + value = "${item?.camera?.offsetX ?: 0}", + ) + val cameraOffsetY = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__edit_camera_offset_y), + value = "${item?.camera?.offsetY ?: 0}", + ) + val sizeWidth = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__edit_size_width), + value = "${item?.size?.width ?: 0}", + ) + val sizeHeight = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__edit_size_height), + value = "${item?.size?.height ?: 0}", + ) + val rootId = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__layer_id), + value = "${item?.resources?.getOrNull(0)?.id ?: 0}", + ) + val rootName = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__layer_name), + value = "${item?.resources?.getOrNull(0)?.name ?: 0}", + ) + val rootUri = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__layer_uri), + value = "${item?.resources?.getOrNull(0)?.uri ?: 0}", + ) + val layerId = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__layer_id), + value = "${item?.resources?.getOrNull(1)?.id ?: 0}", + ) + val layerName = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__layer_name), + value = "${item?.resources?.getOrNull(1)?.name ?: 0}", + ) + val layerUri = createLwaTextFieldFlow( + label = getString(Res.string.game_master__map__layer_uri), + value = "${item?.resources?.getOrNull(1)?.uri ?: 0}", + ) + return GMMapEditViewModel.GMMapEditPageForm( + id = id, + label = label, + cameraZoom = cameraZoom, + cameraOffsetX = cameraOffsetX, + cameraOffsetY = cameraOffsetY, + sizeWidth = sizeWidth, + sizeHeight = sizeHeight, + rootId = rootId, + rootName = rootName, + rootUri = rootUri, + layerId = layerId, + layerName = layerName, + layerUri = layerUri, + ) + } + + fun createPage( + id: String?, + form: GMMapEditViewModel.GMMapEditPageForm, + ): GMMapEditPageUio { + return GMMapEditPageUio( + id = form.id.createLwaTextField(enable = id == null), + label = form.label.createLwaTextField(), + cameraZoom = form.cameraZoom.createLwaTextField(), + cameraOffsetX = form.cameraOffsetX.createLwaTextField(), + cameraOffsetY = form.cameraOffsetY.createLwaTextField(), + sizeWidth = form.sizeWidth.createLwaTextField(), + sizeHeight = form.sizeHeight.createLwaTextField(), + rootId = form.rootId.createLwaTextField(), + rootName = form.rootName.createLwaTextField(), + rootUri = form.rootUri.createLwaTextField(), + layerId = form.layerId.createLwaTextField(), + layerName = form.layerName.createLwaTextField(), + layerUri = form.layerUri.createLwaTextField(), + ) + } + + fun createItem( + form: GMMapEditPageUio, + ): MiniMap? { + return try { + MiniMap( + id = form.id.unpack() ?: "", + name = form.label.unpack() ?: "", + camera = MiniMap.Camera( + zoom = form.cameraZoom.unpack() ?: 1f, + offsetX = form.cameraOffsetX.unpack() ?: 0, + offsetY = form.cameraOffsetY.unpack() ?: 0, + ), + size = MiniMap.Size( + width = form.sizeWidth.unpack() ?: 1, + height = form.sizeHeight.unpack() ?: 1, + ), + resources = listOf( + MiniMap.Resource( + id = form.rootId.unpack() ?: "", + name = form.rootName.unpack() ?: "", + uri = form.rootUri.unpack() ?: "", + ), + MiniMap.Resource( + id = form.layerId.unpack() ?: "", + name = form.layerName.unpack() ?: "", + uri = form.layerUri.unpack() ?: "", + ), + ), + ) + } catch (_: Exception) { + null + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditPage.kt new file mode 100644 index 0000000..997310c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditPage.kt @@ -0,0 +1,396 @@ +package com.pixelized.desktop.lwa.ui.screen.admin.map.edit + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.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.shape.CircleShape +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler +import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMItemEditDestination +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMMapEditDestination +import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.utils.extention.calculateHorizontalPaddings +import com.pixelized.desktop.lwa.utils.extention.calculateVerticalPaddings +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__action__save +import lwacharactersheet.composeapp.generated.resources.game_master__item__create +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_camera_label +import lwacharactersheet.composeapp.generated.resources.game_master__map__edit_size_label +import lwacharactersheet.composeapp.generated.resources.game_master__map__layer_layer_label +import lwacharactersheet.composeapp.generated.resources.game_master__map__layer_root_label +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 GMMapEditPageUio( + val id: LwaTextFieldUio, + val label: LwaTextFieldUio, + val cameraZoom: LwaTextFieldUio, + val cameraOffsetX: LwaTextFieldUio, + val cameraOffsetY: LwaTextFieldUio, + val sizeWidth: LwaTextFieldUio, + val sizeHeight: LwaTextFieldUio, + val rootId: LwaTextFieldUio, + val rootName: LwaTextFieldUio, + val rootUri: LwaTextFieldUio, + val layerId: LwaTextFieldUio, + val layerName: LwaTextFieldUio, + val layerUri: LwaTextFieldUio, +) + +@Stable +object GMMapEditDefault { + val paddings = PaddingValues(all = 8.dp) + + val spacing = 8.dp +} + +@Composable +fun GMMapEditPage( + viewModel: GMMapEditViewModel = koinViewModel(), + paddings: PaddingValues = GMMapEditDefault.paddings, +) { + val screen = LocalScreenController.current + val focus = LocalFocusManager.current + val scope = rememberCoroutineScope() + + val form = viewModel.form.collectAsState() + + GMItemEditContent( + modifier = Modifier.fillMaxSize(), + paddings = paddings, + form = form, + onBack = { + focus.clearFocus(force = true) + screen.navigateBack() + }, + onSave = { + focus.clearFocus(force = true) + scope.launch { + if (viewModel.save()) { + screen.navigateBack() + } + } + }, + ) + + ErrorSnackHandler( + error = viewModel.error, + ) + + ItemEditKeyHandler( + onDismissRequest = { + screen.navigateBack() + }, + ) +} + +@Composable +private fun GMItemEditContent( + modifier: Modifier = Modifier, + paddings: PaddingValues = GMMapEditDefault.paddings, + spacing: Dp = GMMapEditDefault.spacing, + form: State, + onBack: () -> Unit, + onSave: () -> Unit, +) { + val verticalPadding = paddings.calculateVerticalPaddings() + val horizontalPadding = paddings.calculateHorizontalPaddings() + + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(Res.string.game_master__item__create), + ) + }, + navigationIcon = { + IconButton( + onClick = onBack, + ) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null, + ) + } + }, + actions = { + TextButton( + onClick = onSave, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + color = MaterialTheme.lwa.colorScheme.base.primary, + fontWeight = FontWeight.SemiBold, + text = stringResource(Res.string.game_master__action__save), + ) + Icon( + painter = painterResource(Res.drawable.ic_save_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null, + ) + } + } + ) + }, + content = { + AnimatedContent( + targetState = form.value, + transitionSpec = { + if (initialState?.id == targetState?.id) { + EnterTransition.None togetherWith ExitTransition.None + } else { + fadeIn() togetherWith fadeOut() + } + } + ) { + when (it) { + null -> Box( + modifier = Modifier.fillMaxSize(), + ) + + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = verticalPadding, + verticalArrangement = Arrangement.spacedBy(space = spacing), + ) { + item(key = "Id") { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(paddingValues = horizontalPadding), + field = it.id, + singleLine = true, + ) + } + item(key = "Name") { + LwaTextField( + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(paddingValues = horizontalPadding), + field = it.label, + singleLine = true, + ) + } + item(key = "Camera") { + Column( + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(paddingValues = horizontalPadding), + verticalArrangement = Arrangement.spacedBy(space = spacing), + ) { + Text( + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.game_master__map__edit_camera_label) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(space = spacing), + ) { + LwaTextField( + modifier = Modifier.weight(1f), + field = it.cameraZoom, + singleLine = true, + ) + LwaTextField( + modifier = Modifier.weight(1f), + field = it.cameraOffsetX, + singleLine = true, + ) + LwaTextField( + modifier = Modifier.weight(1f), + field = it.cameraOffsetY, + singleLine = true, + ) + } + } + } + item(key = "Size") { + Column( + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(paddingValues = horizontalPadding), + ) { + Text( + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.game_master__map__edit_size_label) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(space = spacing), + ) { + LwaTextField( + modifier = Modifier.weight(1f), + field = it.sizeWidth, + singleLine = true, + ) + LwaTextField( + modifier = Modifier.weight(1f), + field = it.sizeHeight, + singleLine = true, + ) + } + } + } + item(key = "Background") { + Column( + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(paddingValues = horizontalPadding), + verticalArrangement = Arrangement.spacedBy(space = spacing), + ) { + Text( + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.game_master__map__layer_root_label) + ) + LwaTextField( + modifier = Modifier.fillMaxWidth(), + field = it.rootId, + singleLine = true, + ) + LwaTextField( + modifier = Modifier.fillMaxWidth(), + field = it.rootName, + singleLine = true, + ) + LwaTextField( + modifier = Modifier.fillMaxWidth(), + field = it.rootUri, + singleLine = true, + ) + } + } + item(key = "Layer") { + Column( + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(paddingValues = horizontalPadding), + verticalArrangement = Arrangement.spacedBy(space = spacing), + ) { + Text( + style = MaterialTheme.typography.caption, + text = stringResource(Res.string.game_master__map__layer_layer_label) + ) + LwaTextField( + modifier = Modifier.fillMaxWidth(), + field = it.layerId, + singleLine = true, + ) + LwaTextField( + modifier = Modifier.fillMaxWidth(), + field = it.layerName, + singleLine = true, + ) + LwaTextField( + modifier = Modifier.fillMaxWidth(), + field = it.layerUri, + singleLine = true, + ) + } + } + item(key = "Actions") { + Column( + modifier = Modifier + .animateItem() + .fillMaxWidth() + .padding(paddingValues = horizontalPadding), + horizontalAlignment = Alignment.End + ) { + Button( + colors = LwaButtonColors(), + shape = CircleShape, + onClick = onSave, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + text = stringResource(Res.string.game_master__action__save), + ) + Icon( + painter = painterResource(Res.drawable.ic_save_24dp), + contentDescription = null, + ) + } + } + } + } + } + } + } + } + ) +} + +@Composable +private fun ItemEditKeyHandler( + onDismissRequest: () -> Unit, +) { + KeyHandler { + when { + it.type == KeyEventType.KeyDown && it.key == Key.Escape -> { + onDismissRequest() + true + } + + else -> false + } + } +} + +private fun NavHostController.navigateBack() = popBackStack( + route = GMMapEditDestination.baseRoute(), + inclusive = true, +) diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditViewModel.kt new file mode 100644 index 0000000..eded0fa --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/edit/GMMapEditViewModel.kt @@ -0,0 +1,99 @@ +package com.pixelized.desktop.lwa.ui.screen.admin.map.edit + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.network.LwaNetworkException +import com.pixelized.desktop.lwa.repository.map.MapRepository +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldFlow +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.GMMapEditDestination +import com.pixelized.shared.lwa.protocol.rest.APIResponse.ErrorCode +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.runBlocking + +class GMMapEditViewModel( + private val mapRepository: MapRepository, + private val factory: GMMapEditFactory, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val argument = GMMapEditDestination.Argument(savedStateHandle) + + private val _error = MutableSharedFlow() + val error: SharedFlow get() = _error + + private val _form: StateFlow = mapRepository.mapsFlow() + .map { factory.createForm(item = it[argument.id]) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = runBlocking { factory.createForm(item = null) }, + ) + val form: StateFlow = _form + .map { factory.createPage(id = argument.id, form = it) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = runBlocking { factory.createPage(id = argument.id, form = _form.value) }, + ) + + suspend fun save( + form: GMMapEditPageUio = this.form.value, + ): Boolean { + if (!isFormValid()) return false + + val edited = factory.createItem(form = form) ?: return false + + try { + mapRepository.updateMap( + map = edited, + create = argument.id == null, + ) + return true + } catch (exception: LwaNetworkException) { + _form.value.id.errorFlow.value = exception.code == ErrorCode.MapId + val message = ErrorSnackUio.from(exception = exception) + _error.emit(message) + return false + } catch (exception: Exception) { + val message = ErrorSnackUio.from(exception = exception) + _error.emit(message) + return false + } + } + + private suspend fun isFormValid(): Boolean { + var isValid = true + + // check for empty values + _form.value.id.let { field -> + if (field.valueFlow.value.isBlank()) { + field.errorFlow.value = false + isValid = false + } + } + + return isValid + } + + data class GMMapEditPageForm( + val id: LwaTextFieldFlow, + val label: LwaTextFieldFlow, + val cameraZoom: LwaTextFieldFlow, + val cameraOffsetX: LwaTextFieldFlow, + val cameraOffsetY: LwaTextFieldFlow, + val sizeWidth: LwaTextFieldFlow, + val sizeHeight: LwaTextFieldFlow, + val rootId: LwaTextFieldFlow, + val rootName: LwaTextFieldFlow, + val rootUri: LwaTextFieldFlow, + val layerId: LwaTextFieldFlow, + val layerName: LwaTextFieldFlow, + val layerUri: LwaTextFieldFlow, + ) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapFactory.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapFactory.kt new file mode 100644 index 0000000..a73c49a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapFactory.kt @@ -0,0 +1,32 @@ +package com.pixelized.desktop.lwa.ui.screen.admin.map.list + +import com.pixelized.desktop.lwa.utils.extention.unAccent +import java.text.Collator +import com.pixelized.shared.lwa.model.map.Map as MiniMap + +class GMMapFactory() { + fun filterItem( + items: Collection, + unAccentFilter: String, + ): List { + return items.filter { + it.name.unAccent().contains( + other = unAccentFilter, + ignoreCase = true + ) + } + } + + fun convertToGMMapUio( + items: List, + ): List { + return items + .map { item -> + GMMapItemUio( + mapId = item.id, + label = item.name, + ) + } + .sortedWith(compareBy(Collator.getInstance()) { it.label }) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapItem.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapItem.kt new file mode 100644 index 0000000..82eeeeb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapItem.kt @@ -0,0 +1,133 @@ +package com.pixelized.desktop.lwa.ui.screen.admin.map.list + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__item__delete +import lwacharactersheet.composeapp.generated.resources.ic_delete_forever_24dp +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.stringResource + +@Stable +data class GMMapItemUio( + val mapId: String, + val label: String, +) + +@Stable +object GMMapItemDefault { + @Stable + val padding = PaddingValues(start = 16.dp) + + @Stable + val spacing = 4.dp +} + +@Composable +fun GMMapItem( + modifier: Modifier = Modifier, + padding: PaddingValues = GMMapItemDefault.padding, + spacing: Dp = GMMapItemDefault.spacing, + item: GMMapItemUio, + onItem: () -> Unit, + onDelete: () -> Unit, +) { + Row( + modifier = Modifier + .clip(shape = MaterialTheme.lwa.shapes.gameMaster) + .clickable(onClick = onItem) + .background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp) + .minimumInteractiveComponentSize() + .padding(paddingValues = padding) + .then(other = modifier), + horizontalArrangement = Arrangement.spacedBy(space = spacing), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + style = MaterialTheme.lwa.typography.system.body1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = item.label, + ) + OverflowActionMenu( + item = item, + onDelete = onDelete, + ) + } +} + +@Composable +private fun OverflowActionMenu( + modifier: Modifier = Modifier, + item: GMMapItemUio, + onDelete: () -> Unit, +) { + val overflowMenu = remember(item) { + mutableStateOf(false) + } + IconButton( + modifier = modifier, + onClick = { overflowMenu.value = true }, + ) { + Icon( + imageVector = Icons.Default.MoreVert, + tint = MaterialTheme.colors.primary, + contentDescription = null, + ) + DropdownMenu( + expanded = overflowMenu.value, + onDismissRequest = { + overflowMenu.value = false + }, + content = { + DropdownMenuItem( + onClick = { + overflowMenu.value = false + onDelete() + }, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(Res.drawable.ic_delete_forever_24dp), + tint = MaterialTheme.lwa.colorScheme.base.primary, + contentDescription = null, + ) + Text( + style = MaterialTheme.lwa.typography.system.body1, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = stringResource(Res.string.game_master__item__delete), + ) + } + } + }, + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapPage.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapPage.kt new file mode 100644 index 0000000..9737110 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapPage.kt @@ -0,0 +1,159 @@ +package com.pixelized.desktop.lwa.ui.screen.admin.map.list + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler +import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio +import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController +import com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster.navigateToGameMasterMapEditPage +import com.pixelized.desktop.lwa.ui.screen.admin.common.GMFilterHeader +import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__item__create +import org.jetbrains.compose.resources.stringResource +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun GMMapPage( + viewModel: GMMapViewModel = koinViewModel(), +) { + val screen = LocalScreenController.current + val scope = rememberCoroutineScope() + + val items = viewModel.items.collectAsState() + + Box { + GMItemContent( + modifier = Modifier.fillMaxSize(), + filter = viewModel.filter, + items = items, + onItemEdit = { + screen.navigateToGameMasterMapEditPage(mapId = it) + }, + onItemDelete = { + scope.launch { + viewModel.deleteMap(mapId = it) + } + }, + onItemCreate = { + screen.navigateToGameMasterMapEditPage(mapId = null) + }, + ) + + ErrorSnackHandler( + error = viewModel.error, + ) + } +} + +@Composable +private fun GMItemContent( + modifier: Modifier = Modifier, + padding: Dp = 8.dp, + spacing: Dp = 8.dp, + filter: LwaTextFieldUio, + items: State>, + onItemEdit: (String) -> Unit, + onItemDelete: (String) -> Unit, + onItemCreate: () -> Unit, +) { + Column( + modifier = modifier, + ) { + Surface( + modifier = Modifier.zIndex(1f), // avoid display issue with LazyColumnScope.animateItem() + elevation = 1.dp, + ) { + GMFilterHeader( + padding = padding, + spacing = spacing, + filter = filter, + tags = remember { mutableStateOf(emptyList()) }, + onTag = { }, + ) + } + Box( + modifier = Modifier.fillMaxWidth().weight(1f), + ) { + LazyColumn( + modifier = Modifier.matchParentSize().clipToBounds(), + contentPadding = remember { + PaddingValues( + start = padding, + top = padding, + end = padding, + bottom = padding + 48.dp + padding, + ) + }, + verticalArrangement = Arrangement.spacedBy(space = spacing), + ) { + items( + items = items.value, + key = { it.mapId }, + ) { item -> + GMMapItem( + modifier = Modifier + .fillMaxWidth() + .animateItem(), + item = item, + onItem = { + onItemEdit(item.mapId) + }, + onDelete = { + onItemDelete(item.mapId) + }, + ) + } + } + Row( + modifier = Modifier + .align(alignment = Alignment.BottomEnd) + .padding(all = padding), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Button( + colors = LwaButtonColors(), + shape = CircleShape, + onClick = onItemCreate, + ) { + Text( + modifier = Modifier.padding(end = 4.dp), + text = stringResource(Res.string.game_master__item__create), + ) + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + ) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapViewModel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapViewModel.kt new file mode 100644 index 0000000..e1d0907 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/admin/map/list/GMMapViewModel.kt @@ -0,0 +1,59 @@ +package com.pixelized.desktop.lwa.ui.screen.admin.map.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.desktop.lwa.repository.map.MapRepository +import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio +import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextField +import com.pixelized.desktop.lwa.ui.composable.textfield.createLwaTextFieldFlow +import com.pixelized.desktop.lwa.utils.extention.unAccent +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.runBlocking +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.game_master__character__filter +import org.jetbrains.compose.resources.getString + +class GMMapViewModel( + private val mapRepository: MapRepository, + itemFactory: GMMapFactory, +) : ViewModel() { + + private val _error = MutableSharedFlow() + val error: SharedFlow get() = _error + + private val _filter = createLwaTextFieldFlow( + label = runBlocking { getString(Res.string.game_master__character__filter) }, + ) + val filter = _filter.createLwaTextField() + + val items: StateFlow> = combine( + mapRepository.mapsFlow(), + filter.valueFlow.map { it.unAccent() }, + ) { items, unAccentFilter -> + itemFactory.convertToGMMapUio( + items = itemFactory.filterItem( + items = items.values, + unAccentFilter = unAccentFilter, + ), + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + + suspend fun deleteMap(mapId: String) { + try { + mapRepository.deleteMap(mapId = mapId) + } catch (exception: Exception) { + val message = ErrorSnackUio.from(exception = exception) + _error.emit(message) + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/PUT_Map.kt b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/PUT_Map.kt index bbfee37..e66fc68 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/PUT_Map.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/server/rest/map/PUT_Map.kt @@ -15,9 +15,11 @@ fun Engine.putMap(): suspend RoutingContext.() -> Unit { return { try { val form = call.receive() + val create = call.queryParameters.create mapService.save( json = form, + create = create, ) call.respond( message = APIResponse.success(), diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapService.kt index c7002d2..83e9aaa 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapService.kt @@ -19,9 +19,11 @@ class MapService( @Throws suspend fun save( json: MapJson, + create: Boolean, ) { mapStore.save( map = json, + create = create, ) } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapStore.kt index fac94cd..0c2e45f 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/services/map/MapStore.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File -import java.text.Collator class MapStore( private val pathProvider: PathProvider, @@ -74,18 +73,27 @@ class MapStore( } @Throws(JsonConversionException::class, FileWriteException::class) - suspend fun save(map: MapJson) { - // convert the data to json format - val json = try { + suspend fun save( + map: MapJson, + create: Boolean, + ) { + val file = mapFile(id = map.id) + // Guard case on update alteration + if (create && file.exists()) { + throw BusinessException( + message = "Map already exist, creation is impossible.", + ) + } + // Encode the json into a string. + val data = try { this.jsonSerializer.encodeToString(map) } catch (exception: Exception) { throw JsonConversionException(root = exception) } - // write the file + // write the map into a file try { - val file = mapFile(id = map.id) file.writeText( - text = json, + text = data, charset = Charsets.UTF_8, ) } catch (exception: Exception) { @@ -105,13 +113,13 @@ class MapStore( // Guard case on the file existence. if (file.exists().not()) { throw BusinessException( - message = "Alteration doesn't not exist, deletion is impossible.", + message = "Map doesn't not exist, deletion is impossible.", ) } // Guard case on the file deletion if (file.delete().not()) { throw BusinessException( - message = "Alteration file have not been deleted for unknown reason.", + message = "Map file have not been deleted for unknown reason.", ) } // Update the data model with the deleted alteration. diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt b/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt index 2abc6ef..3141426 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/utils/extentions/ParametersExt.kt @@ -46,7 +46,7 @@ val Parameters.itemId: String val Parameters.mapId: String get() = param( name = "mapId", - code = APIResponse.ErrorCode.ItemId, + code = APIResponse.ErrorCode.MapId, ) val Parameters.count: Float diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/Map.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/Map.kt index 7ea8a98..0b0d790 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/Map.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/model/map/Map.kt @@ -2,7 +2,7 @@ package com.pixelized.shared.lwa.model.map data class Map( val id: String, - val name : String, + val name: String, val camera: Camera, val size: Size, val resources: List, diff --git a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt index b58c0e9..0c88f87 100644 --- a/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt +++ b/shared/src/commonMain/kotlin/com/pixelized/shared/lwa/protocol/rest/APIResponse.kt @@ -18,6 +18,7 @@ data class APIResponse( InventoryId, ItemName, CharacterSheetId, + MapId, Create, Active, Equip,