Fix a crash related to default network config.

This commit is contained in:
Thomas Andres Gomez 2025-03-17 13:32:49 +01:00
parent a617d4ba7b
commit e2b3b54f88
18 changed files with 153 additions and 119 deletions

View file

@ -36,7 +36,7 @@ class DataSyncViewModel(
networkRepository.status
.filter { status -> status == NetworkRepository.Status.CONNECTED }
.onEach { campaignRepository.update() }
.onEach { campaignRepository.campaign(update = true) }
.launchIn(this)
networkRepository.status

View file

@ -16,27 +16,27 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsStore
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterDetailCharacteristicDialogViewModel
import com.pixelized.desktop.lwa.ui.composable.character.characteristic.CharacterSheetCharacteristicDialogFactory
import com.pixelized.desktop.lwa.ui.overlay.roll.RollViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.TextMessageFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDiminishedViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.TextMessageFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEditViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory
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.campaign.network.NetworkFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterActionUseCase
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel
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
import com.pixelized.desktop.lwa.ui.screen.settings.SettingsViewModel
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
@ -77,7 +77,9 @@ val toolsDependencies
}
}
single<LwaClient> {
LwaClientImpl(get(), get())
val client: HttpClient = get()
val setting: SettingsRepository = get()
LwaClientImpl(client, setting)
}
single {
PathProvider(appName = "LwaClient")

View file

@ -17,8 +17,8 @@ class CampaignRepository(
val campaignFlow get() = store.campaignFlow
suspend fun update() {
store.update()
suspend fun campaign(update: Boolean) {
store.campaign(update = update)
}
fun instanceFlow(

View file

@ -30,57 +30,78 @@ class CampaignStore(
init {
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
update()
campaign(update = true)
}
scope.launch {
network.data.collect(::handleMessage)
}
}
suspend fun update() {
_campaignFlow.value = load()
}
private suspend fun load(): Campaign {
val request = client.campaign()
val data = factory.convertFromJson(json = request)
return data
suspend fun campaign(update: Boolean): Campaign {
val campaign = _campaignFlow.value
if (update || campaign == Campaign.EMPTY) {
val data = try {
val request = client.campaign()
factory.convertFromJson(json = request)
} catch (exception: Exception) {
println(exception) // TODO
Campaign.EMPTY
}
return _campaignFlow.update(data)
}
return campaign
}
suspend fun addCharacter(
characterSheetId: String,
) {
client.campaignAddCharacter(
characterSheetId = characterSheetId
)
try {
client.campaignAddCharacter(
characterSheetId = characterSheetId
)
} catch (exception: Exception) {
println(exception) // TODO
}
}
suspend fun removeCharacter(
characterSheetId: String,
instanceId: Int,
) {
client.campaignRemoveCharacter(
characterSheetId = characterSheetId,
instanceId = instanceId,
)
try {
client.campaignRemoveCharacter(
characterSheetId = characterSheetId,
instanceId = instanceId,
)
} catch (exception: Exception) {
println(exception) // TODO
}
}
suspend fun addNpc(
characterSheetId: String,
) {
client.campaignAddNpc(
characterSheetId = characterSheetId
)
try {
client.campaignAddNpc(
characterSheetId = characterSheetId
)
} catch (exception: Exception) {
println(exception) // TODO
}
}
suspend fun removeNpc(
characterSheetId: String,
instanceId: Int,
) {
client.campaignRemoveNpc(
characterSheetId = characterSheetId,
instanceId = instanceId,
)
try {
client.campaignRemoveNpc(
characterSheetId = characterSheetId,
instanceId = instanceId,
)
} catch (exception: Exception) {
println(exception) // TODO
}
}
// region : WebSocket message Handling.
@ -88,7 +109,7 @@ class CampaignStore(
private suspend fun handleMessage(message: Message) {
when (val payload = message.value) {
is RestSynchronisation.Campaign -> {
update()
campaign(update = true)
}
is CampaignMessage -> {
@ -177,4 +198,9 @@ class CampaignStore(
}
// endregion
private fun MutableStateFlow<Campaign>.update(campaign: Campaign): Campaign {
value = campaign
return campaign
}
}

View file

@ -1,7 +0,0 @@
package com.pixelized.desktop.lwa.repository.campaign.model
data class CharacterSheetPreview(
val characterSheetId: String,
val name: String,
val level: Int,
)

View file

@ -1,6 +1,6 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -20,7 +20,7 @@ class CharacterSheetRepository(
val characterDetailFlow get() = store.detailFlow
suspend fun updateCharacterPreviews() {
store.updateCharacterPreviews()
store.charactersPreview()
}
fun characterPreview(characterId: String?): CharacterSheetPreview? {
@ -33,8 +33,8 @@ class CharacterSheetRepository(
): CharacterSheet? {
return try {
characterSheetId?.let {
store.characterDetail(
characterId = it,
store.getCharacterSheet(
characterSheetId = it,
forceUpdate = forceUpdate
)
}
@ -61,12 +61,12 @@ class CharacterSheetRepository(
suspend fun updateCharacter(
characterSheet: CharacterSheet,
) {
store.updateCharacter(sheet = characterSheet)
store.updateCharacterSheet(sheet = characterSheet)
}
suspend fun deleteCharacter(
characterId: String,
) {
store.deleteCharacter(characterId = characterId)
store.deleteCharacterSheet(characterId = characterId)
}
}

View file

@ -1,10 +1,10 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.network.LwaClient
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview
import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage
@ -32,7 +32,7 @@ class CharacterSheetStore(
val scope = CoroutineScope(Dispatchers.IO + Job())
// initial data loading.
scope.launch {
updateCharacterPreviews()
charactersPreview()
}
// data update through WebSocket.
scope.launch {
@ -42,58 +42,55 @@ class CharacterSheetStore(
// region Rest
suspend fun updateCharacterPreviews() {
_previewFlow.value = charactersPreview()
}
suspend fun charactersPreview(): List<CharacterSheetPreview> {
val request = client.characters()
val data = request.map {
CharacterSheetPreview(
characterSheetId = it.id,
name = it.name,
level = it.level,
)
val request = try {
client.characters()
} catch (exception: Exception) {
println(exception) // TODO
emptyList()
}
return data
val characters = request.map { factory.convertFromJson(it) }
return _previewFlow.update(characters)
}
suspend fun characterDetail(
characterId: String,
suspend fun getCharacterSheet(
characterSheetId: String,
forceUpdate: Boolean = false,
): CharacterSheet {
val character = _detailFlow.value[characterId]
): CharacterSheet? {
val character = _detailFlow.value[characterSheetId]
if (forceUpdate || character == null) {
val request = client.character(id = characterId)
val data = factory.convertFromJson(json = request)
// update the local detail flow.
return _detailFlow.update(data)
} else {
return character
try {
val request = client.character(id = characterSheetId)
val data = factory.convertFromJson(json = request)
return _detailFlow.update(data)
} catch (exception: Exception) {
println(exception) // TODO
return null
}
}
return character
}
suspend fun updateCharacter(
suspend fun updateCharacterSheet(
sheet: CharacterSheet,
) {
val json = factory.convertToJson(sheet = sheet)
try {
client.updateCharacter(sheet = json)
} catch (exception: Exception) {
// TODO
println(exception)
println(exception) // TODO
}
_detailFlow.update(sheet = sheet)
}
suspend fun deleteCharacter(
suspend fun deleteCharacterSheet(
characterId: String,
) {
try {
client.deleteCharacterSheet(id = characterId)
_detailFlow.delete(characterId = characterId)
} catch (exception: Exception) {
// TODO
println(exception) // TODO
}
}
@ -102,8 +99,8 @@ class CharacterSheetStore(
private suspend fun handleMessage(message: Message) {
when (val payload = message.value) {
is RestSynchronisation.CharacterUpdate -> characterDetail(
characterId = payload.id,
is RestSynchronisation.CharacterUpdate -> getCharacterSheet(
characterSheetId = payload.id,
forceUpdate = true
)
@ -130,24 +127,38 @@ class CharacterSheetStore(
skillId: String,
used: Boolean,
) {
val sheet = getCharacterSheet(characterSheetId = characterId) ?: return
val character = useCase.updateSkillUsage(
character = characterDetail(characterId = characterId),
character = sheet,
skillId = skillId,
used = used,
)
_detailFlow.update(character)
}
// endregion
private fun MutableStateFlow<Map<String, CharacterSheet>>.update(sheet: CharacterSheet): CharacterSheet {
private fun MutableStateFlow<List<CharacterSheetPreview>>.update(
previews: List<CharacterSheetPreview>,
): List<CharacterSheetPreview> {
value = previews
return previews
}
private fun MutableStateFlow<Map<String, CharacterSheet>>.update(
sheet: CharacterSheet,
): CharacterSheet {
value = value.toMutableMap().also {
it[sheet.id] = sheet
}
return sheet
}
private fun MutableStateFlow<Map<String, CharacterSheet>>.delete(characterId: String) {
private fun MutableStateFlow<Map<String, CharacterSheet>>.delete(
characterId: String,
) {
value = value.toMutableMap().also {
it.remove(characterId)
}

View file

@ -22,7 +22,7 @@ class SettingsFactory(
autoShowChat = settings.autoShowChat,
autoScrollChat = settings.autoScrollChat,
isAdmin = settings.isAdmin,
isGM = settings.isGM,
isGameMaster = settings.isGameMaster,
)
}
@ -48,7 +48,7 @@ class SettingsFactory(
autoShowChat = json.autoShowChat ?: default.autoShowChat,
autoScrollChat = json.autoScrollChat ?: default.autoScrollChat,
isAdmin = json.isAdmin ?: default.isAdmin,
isGM = json.isGM ?: default.isGM,
isGameMaster = json.isGameMaster ?: default.isGameMaster,
)
}
}

View file

@ -1,30 +1,16 @@
package com.pixelized.desktop.lwa.repository.settings
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
class SettingsRepository(
private val store: SettingsStore,
private val useCase: SettingsUseCase,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
fun settingsFlow(): StateFlow<Settings> = store.settingsFlow
private val settings = store.settingsFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = useCase.defaultSettings()
)
fun settingsFlow(): StateFlow<Settings> = settings
fun settings(): Settings = settings.value
fun settings(): Settings = store.settingsFlow.value
fun update(settings: Settings) {
store.save(settings = settings)

View file

@ -4,12 +4,9 @@ import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.repository.settings.model.SettingsJson
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import java.io.File
@ -19,17 +16,19 @@ class SettingsStore(
private val useCase: SettingsUseCase,
private val jsonFormatter: Json,
) {
private val flow = MutableStateFlow(value = useCase.defaultSettings())
fun settingsFlow(): StateFlow<Settings> = flow
private val _settingsFlow = MutableStateFlow(value = useCase.defaultSettings())
val settingsFlow: StateFlow<Settings> = _settingsFlow
init {
// create the directory
File(pathProvider.storePath()).also { it.mkdirs() }
// load the data.
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
flow.value = load()
runBlocking {
try {
_settingsFlow.value = load()
} catch (exception: Exception) {
println(exception) // TODO
}
}
}
@ -55,7 +54,7 @@ class SettingsStore(
root = exception
)
}
flow.value = settings
_settingsFlow.value = settings
}
private fun load(): Settings {

View file

@ -9,8 +9,8 @@ data class Settings(
val autoHideDelay: Int,
val autoShowChat: Boolean,
val autoScrollChat: Boolean,
val isAdmin: Boolean,
val isGM: Boolean,
val isAdmin: Boolean? = null,
val isGameMaster: Boolean? = null,
) {
val root: String get() = "http://${"${host}:${port}".removePrefix("http://")}"
}

View file

@ -12,6 +12,6 @@ data class SettingsJsonV1(
val autoHideDelay: Int?,
val autoShowChat: Boolean?,
val autoScrollChat: Boolean?,
val isGM: Boolean?,
val isGameMaster: Boolean?,
val isAdmin: Boolean?,
) : SettingsJson

View file

@ -68,9 +68,9 @@ abstract class CharacterRibbonViewModel(
alterations = alterations,
characterInstanceId = entry.key,
characterInstance = entry.value,
enableCharacterId = settings.isGM,
enableCharacterSheet = enableCharacterSheet || settings.isGM,
enableCharacterStats = enableCharacterStats || settings.isGM,
enableCharacterId = settings.isGameMaster ?: false,
enableCharacterSheet = enableCharacterSheet || settings.isGameMaster ?: false,
enableCharacterStats = enableCharacterStats || settings.isGameMaster ?: false,
)
}
},

View file

@ -26,7 +26,7 @@ class CampaignToolbarViewModel(
)
val isAdmin = settingsRepository.settingsFlow()
.map { it.isAdmin }
.map { it.isAdmin ?: false }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,

View file

@ -1,6 +1,6 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio

View file

@ -76,7 +76,7 @@ class GameMasterViewModel(
val settings = settingsRepository.settings()
settingsRepository.update(
settings = settings.copy(
isGM = settings.isGM.not(),
isGameMaster = (settings.isGameMaster ?: false).not(),
)
)
}

View file

@ -13,8 +13,8 @@ class SettingsUseCase {
autoHideDelay = 8,
autoShowChat = true,
autoScrollChat = true,
isAdmin = false,
isGM = false,
isAdmin = null,
isGameMaster = null,
)
companion object {