Add map admin feature.
This commit is contained in:
parent
db98fbede7
commit
2b09126650
26 changed files with 1214 additions and 32 deletions
|
|
@ -316,8 +316,8 @@
|
|||
<string name="game_master__item__edit_id">Identifiant de l'objet</string>
|
||||
<string name="game_master__item__edit_label">Nom</string>
|
||||
<string name="game_master__item__edit_description">Description</string>
|
||||
<string name="game_master__item__edit_image">Image url</string>
|
||||
<string name="game_master__item__edit_thumbnail">Vignette url</string>
|
||||
<string name="game_master__item__edit_image">Url de l'image</string>
|
||||
<string name="game_master__item__edit_thumbnail">Url de la vignette</string>
|
||||
<string name="game_master__item__edit_stackable">Empilable</string>
|
||||
<string name="game_master__item__edit_equipable">Équipable</string>
|
||||
<string name="game_master__item__edit_consumable">Consommable</string>
|
||||
|
|
@ -337,4 +337,19 @@
|
|||
<string name="game_master__actions__show_npc__title">Montrer le groupe de npcs</string>
|
||||
<string name="game_master__actions__show_npc__description">Montrer le panneau latéral droit pour tous les joueurs.</string>
|
||||
|
||||
<string name="game_master__map__edit_id">Identifiant de la carte</string>
|
||||
<string name="game_master__map__edit_name">Nom de la carte</string>
|
||||
<string name="game_master__map__edit_camera_label">Camera</string>
|
||||
<string name="game_master__map__edit_camera_zoom">Zoom</string>
|
||||
<string name="game_master__map__edit_camera_offset_x">X</string>
|
||||
<string name="game_master__map__edit_camera_offset_y">Y</string>
|
||||
<string name="game_master__map__edit_size_label">Taille</string>
|
||||
<string name="game_master__map__edit_size_width">Largeur</string>
|
||||
<string name="game_master__map__edit_size_height">Hauteur</string>
|
||||
<string name="game_master__map__layer_root_label">Carte</string>
|
||||
<string name="game_master__map__layer_layer_label">Region</string>
|
||||
<string name="game_master__map__layer_id">Identifiant de la couche</string>
|
||||
<string name="game_master__map__layer_name">Nom de la couche</string>
|
||||
<string name="game_master__map__layer_uri">Url de l'image</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -177,6 +177,10 @@ interface LwaClient {
|
|||
create: Boolean,
|
||||
): APIResponse<Unit>
|
||||
|
||||
suspend fun deleteMap(
|
||||
mapId: String,
|
||||
): APIResponse<Unit>
|
||||
|
||||
companion object {
|
||||
fun error(error: APIResponse<*>): Nothing = throw LwaNetworkException(error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,9 +299,16 @@ class LwaClientImpl(
|
|||
mapJson: MapJson,
|
||||
create: Boolean,
|
||||
): APIResponse<Unit> = client
|
||||
.put("$root/map/update") {
|
||||
.put("$root/map/update?create=$create") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapJson)
|
||||
}
|
||||
.body<APIResponse<Unit>>()
|
||||
|
||||
@Throws
|
||||
override suspend fun deleteMap(
|
||||
mapId: String,
|
||||
): APIResponse<Unit> = client
|
||||
.delete("$root/map/delete?mapId=$mapId")
|
||||
.body<APIResponse<Unit>>()
|
||||
}
|
||||
|
|
@ -35,4 +35,11 @@ class MapRepository(
|
|||
create = create,
|
||||
)
|
||||
}
|
||||
|
||||
@Throws
|
||||
suspend fun deleteMap(
|
||||
mapId: String,
|
||||
) {
|
||||
mapStore.deleteMap(mapId = mapId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ class TagStore(
|
|||
private val characterTagsFlow = MutableStateFlow<Map<String, Tag>>(emptyMap())
|
||||
private val alterationTagsFlow = MutableStateFlow<Map<String, Tag>>(emptyMap())
|
||||
private val itemTagsFlow = MutableStateFlow<Map<String, Tag>>(emptyMap())
|
||||
private val mapTagsFlow = MutableStateFlow<Map<String, Tag>>(emptyMap())
|
||||
|
||||
fun charactersTagFlow(): StateFlow<Map<String, Tag>> = characterTagsFlow
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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<String>(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)
|
||||
}
|
||||
|
|
@ -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<NavHostController> {
|
||||
error("GameMaster NavHost controller is not yet ready")
|
||||
|
|
@ -38,9 +39,11 @@ fun GameMasterNavHost() {
|
|||
startDestination = GameMasterDestination.navigationRoute(),
|
||||
) {
|
||||
composableGameMasterMainPage()
|
||||
|
||||
composableGameMasterCharacterEditPage()
|
||||
composableGameMasterAlterationEditPage()
|
||||
composableGameMasterItemEditPage()
|
||||
composableGameMasterMapEditPage()
|
||||
|
||||
composableLevelUp()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<GMMapEditPageUio?>,
|
||||
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,
|
||||
)
|
||||
|
|
@ -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<ErrorSnackUio>()
|
||||
val error: SharedFlow<ErrorSnackUio> get() = _error
|
||||
|
||||
private val _form: StateFlow<GMMapEditPageForm> = mapRepository.mapsFlow()
|
||||
.map { factory.createForm(item = it[argument.id]) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = runBlocking { factory.createForm(item = null) },
|
||||
)
|
||||
val form: StateFlow<GMMapEditPageUio> = _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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MiniMap>,
|
||||
unAccentFilter: String,
|
||||
): List<MiniMap> {
|
||||
return items.filter {
|
||||
it.name.unAccent().contains(
|
||||
other = unAccentFilter,
|
||||
ignoreCase = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun convertToGMMapUio(
|
||||
items: List<MiniMap>,
|
||||
): List<GMMapItemUio> {
|
||||
return items
|
||||
.map { item ->
|
||||
GMMapItemUio(
|
||||
mapId = item.id,
|
||||
label = item.name,
|
||||
)
|
||||
}
|
||||
.sortedWith(compareBy(Collator.getInstance()) { it.label })
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<GMMapItemUio>>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ErrorSnackUio>()
|
||||
val error: SharedFlow<ErrorSnackUio> get() = _error
|
||||
|
||||
private val _filter = createLwaTextFieldFlow(
|
||||
label = runBlocking { getString(Res.string.game_master__character__filter) },
|
||||
)
|
||||
val filter = _filter.createLwaTextField()
|
||||
|
||||
val items: StateFlow<List<GMMapItemUio>> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,11 @@ fun Engine.putMap(): suspend RoutingContext.() -> Unit {
|
|||
return {
|
||||
try {
|
||||
val form = call.receive<MapJson>()
|
||||
val create = call.queryParameters.create
|
||||
|
||||
mapService.save(
|
||||
json = form,
|
||||
create = create,
|
||||
)
|
||||
call.respond(
|
||||
message = APIResponse.success(),
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ class MapService(
|
|||
@Throws
|
||||
suspend fun save(
|
||||
json: MapJson,
|
||||
create: Boolean,
|
||||
) {
|
||||
mapStore.save(
|
||||
map = json,
|
||||
create = create,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Resource>,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ data class APIResponse<T>(
|
|||
InventoryId,
|
||||
ItemName,
|
||||
CharacterSheetId,
|
||||
MapId,
|
||||
Create,
|
||||
Active,
|
||||
Equip,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue