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