Update the old / new UI to work with the server.

This commit is contained in:
Thomas Andres Gomez 2025-02-24 17:12:50 +01:00
parent bd4d65fe6a
commit ed1b27039d
40 changed files with 568 additions and 390 deletions

View file

@ -1,7 +1,7 @@
plugins { plugins {
alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinJvm) apply false alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.kotlinMultiplatform) apply false
} }

View file

@ -1,10 +1,10 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins { plugins {
alias(libs.plugins.composeCompiler)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization) alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
} }
kotlin { kotlin {

View file

@ -14,4 +14,7 @@
# Serialization # Serialization
-keep class io.ktor.serialization.kotlinx.json.** { *; } -keep class io.ktor.serialization.kotlinx.json.** { *; }
-keep class com.pixelized.shared.lwa.model.** { *; } -keep class com.pixelized.shared.lwa.model.** { *; }
-keep class com.pixelized.shared.lwa.protocol.rest.** { *; }
-keep class com.pixelized.desktop.lwa.repository.settings.model.** { *; }
-keep @kotlinx.serialization.Serializable class * { *; }

View file

@ -29,8 +29,11 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.ApplicationScope import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.Window import androidx.compose.ui.window.Window
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.ui.composable.key.KeyEventHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyEventHandler
import com.pixelized.desktop.lwa.ui.composable.key.LocalKeyEventHandlers import com.pixelized.desktop.lwa.ui.composable.key.LocalKeyEventHandlers
import com.pixelized.desktop.lwa.ui.navigation.screen.MainNavHost
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetEditDestination import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.ui.navigation.window.WindowController import com.pixelized.desktop.lwa.ui.navigation.window.WindowController
@ -39,12 +42,8 @@ import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheet
import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetWindow import com.pixelized.desktop.lwa.ui.navigation.window.destination.CharacterSheetWindow
import com.pixelized.desktop.lwa.ui.navigation.window.destination.RollHistoryWindow import com.pixelized.desktop.lwa.ui.navigation.window.destination.RollHistoryWindow
import com.pixelized.desktop.lwa.ui.navigation.window.rememberMaxWindowHeight import com.pixelized.desktop.lwa.ui.navigation.window.rememberMaxWindowHeight
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignScreen
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.ui.navigation.screen.MainNavHost
import com.pixelized.desktop.lwa.ui.screen.campaign.player.CampaignScreen
import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost import com.pixelized.desktop.lwa.ui.screen.characterSheet.CharacterSheetMainNavHost
import com.pixelized.desktop.lwa.ui.screen.network.NetworkViewModel
import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage import com.pixelized.desktop.lwa.ui.screen.rollhistory.RollHistoryPage
import com.pixelized.desktop.lwa.ui.theme.LwaTheme import com.pixelized.desktop.lwa.ui.theme.LwaTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -54,7 +53,6 @@ import lwacharactersheet.composeapp.generated.resources.network__disconnect__mes
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
val LocalWindowController = compositionLocalOf<WindowController> { val LocalWindowController = compositionLocalOf<WindowController> {
error("Local Window Controller is not yet ready") error("Local Window Controller is not yet ready")
@ -127,8 +125,8 @@ fun ApplicationScope.App() {
} }
}, },
content = { content = {
MainNavHost() // MainNavHost()
// CampaignScreen() CampaignScreen()
} }
) )
NetworkSnackHandler( NetworkSnackHandler(

View file

@ -4,6 +4,8 @@ import com.pixelized.desktop.lwa.business.ExpressionUseCase
import com.pixelized.desktop.lwa.business.RollUseCase import com.pixelized.desktop.lwa.business.RollUseCase
import com.pixelized.desktop.lwa.business.SettingsUseCase import com.pixelized.desktop.lwa.business.SettingsUseCase
import com.pixelized.desktop.lwa.business.SkillStepUseCase import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.network.LwaClient
import com.pixelized.desktop.lwa.network.LwaClientImpl
import com.pixelized.desktop.lwa.parser.dice.DiceParser import com.pixelized.desktop.lwa.parser.dice.DiceParser
import com.pixelized.desktop.lwa.parser.expression.ExpressionParser import com.pixelized.desktop.lwa.parser.expression.ExpressionParser
import com.pixelized.desktop.lwa.parser.word.WordParser import com.pixelized.desktop.lwa.parser.word.WordParser
@ -20,6 +22,7 @@ import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsStore import com.pixelized.desktop.lwa.repository.settings.SettingsStore
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailViewModel 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.detail.CharacterDiminishedViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.CharacterSheetFactory 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.detail.CharacterSheetViewModel
@ -72,6 +75,7 @@ val toolsDependencies
} }
} }
} }
single<LwaClient> { LwaClientImpl(get(), get()) }
} }
val storeDependencies val storeDependencies
@ -100,6 +104,7 @@ val factoryDependencies
factoryOf(::SkillFieldFactory) factoryOf(::SkillFieldFactory)
factoryOf(::SettingsFactory) factoryOf(::SettingsFactory)
factoryOf(::CampaignJsonFactory) factoryOf(::CampaignJsonFactory)
factoryOf(::PlayerRibbonFactory)
} }
val viewModelDependencies val viewModelDependencies

View file

@ -5,6 +5,13 @@ import com.pixelized.desktop.lwa.repository.settings.model.Settings
class SettingsUseCase { class SettingsUseCase {
fun defaultSettings(): Settings = Settings( fun defaultSettings(): Settings = Settings(
host = DEFAULT_HOST,
port = DEFAULT_PORT,
playerName = "", playerName = "",
) )
companion object {
private const val DEFAULT_HOST = "http://pixelized.freeboxos.fr"
private const val DEFAULT_PORT = 16030
}
} }

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.network
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
interface LwaClient {
suspend fun characters(): List<CharacterPreviewJson>
suspend fun character(id: String): CharacterSheetJson
suspend fun updateCharacter(sheet: CharacterSheetJson)
suspend fun deleteCharacter(id: String)
suspend fun campaign(): CampaignJson
suspend fun campaignAddCharacter(id: String)
suspend fun campaignDeleteCharacter(id: String)
suspend fun campaignAddNpc(id: String)
suspend fun campaignDeleteNpc(id: String)
}

View file

@ -0,0 +1,64 @@
package com.pixelized.desktop.lwa.network
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.protocol.rest.CharacterPreviewJson
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.http.path
class LwaClientImpl(
private val client: HttpClient,
setting: SettingsRepository,
) : LwaClient {
private val root = setting.settings().root
override suspend fun characters(): List<CharacterPreviewJson> = client
.get("$root/characters")
.body()
override suspend fun character(id: String): CharacterSheetJson = client
.get("$root/character/detail?id=$id")
.body()
override suspend fun updateCharacter(sheet: CharacterSheetJson) = client
.put {
url {
path("$host/character/update?id=")
contentType(ContentType.Application.Json)
setBody(sheet)
}
}
.body<Unit>()
override suspend fun deleteCharacter(id: String) = client
.delete("$root/character/delete?id=$id")
.body<Unit>()
override suspend fun campaign(): CampaignJson = client
.get("$root/campaign")
.body()
override suspend fun campaignAddCharacter(id: String) = client
.put("$root/campaign/character/update?id=$id")
.body<Unit>()
override suspend fun campaignDeleteCharacter(id: String) = client
.delete("$root/campaign/character/delete?id=$id")
.body<Unit>()
override suspend fun campaignAddNpc(id: String) = client
.put("$root/campaign/npc/update?id=$id")
.body<Unit>()
override suspend fun campaignDeleteNpc(id: String) = client
.delete("$root/campaign/npc/delete?id=$id")
.body<Unit>()
}

View file

@ -1,6 +1,7 @@
package com.pixelized.desktop.lwa.repository.campaign package com.pixelized.desktop.lwa.repository.campaign
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.character
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -10,31 +11,21 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
class CampaignRepository( class CampaignRepository(
store: CampaignStore, private val store: CampaignStore,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO + Job()) private val scope = CoroutineScope(Dispatchers.IO + Job())
private val campaign = store.campaignFlow() val campaignFlow get() = store.campaignFlow
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = Campaign.EMPTY,
)
fun campaignFlow(): StateFlow<Campaign> = campaign fun characterInstanceFlow(id: String): StateFlow<Campaign.CharacterInstance> {
return campaignFlow
fun characterInstance(id: String): StateFlow<Campaign.CharacterInstance> {
return campaign
.mapNotNull { .mapNotNull {
it.characters[id] it.characters[id]
} }
.stateIn( .stateIn(
scope = scope, scope = scope,
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
initialValue = campaign.value.characters[id] ?: Campaign.CharacterInstance( initialValue = campaignFlow.value.character(id = id),
characteristic = emptyMap(),
usedSkill = emptyList(),
)
) )
} }
} }

View file

@ -1,72 +1,82 @@
package com.pixelized.desktop.lwa.repository.campaign package com.pixelized.desktop.lwa.repository.campaign
import com.pixelized.desktop.lwa.network.LwaClient
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.campaign.Campaign import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.CampaignJson
import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory import com.pixelized.shared.lwa.model.campaign.CampaignJsonFactory
import com.pixelized.shared.lwa.protocol.MessageType import com.pixelized.shared.lwa.model.campaign.character
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage 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.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.usecase.CampaignUseCase import com.pixelized.shared.lwa.usecase.CampaignUseCase
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlin.collections.set
class CampaignStore( class CampaignStore(
private val client: LwaClient,
private val network: NetworkRepository, private val network: NetworkRepository,
private val factory: CampaignJsonFactory, private val factory: CampaignJsonFactory,
private val useCase: CampaignUseCase, private val useCase: CampaignUseCase,
private val client: HttpClient,
private val json: Json,
) { ) {
private val flow = MutableStateFlow(value = Campaign.EMPTY) private val _campaignFlow = MutableStateFlow(value = Campaign.EMPTY)
val campaignFlow: StateFlow<Campaign> get() = _campaignFlow
init { init {
val scope = CoroutineScope(Dispatchers.IO + Job()) val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch { scope.launch {
flow.value = load() update()
} }
scope.launch { scope.launch {
network.data network.data.collect(::handleMessage)
.mapNotNull { it.takeIf { it.type == MessageType.UpdatePlayerCharacteristic } }
.map { json.decodeFromString<UpdatePlayerCharacteristicMessage>(it.value) }
.collect {
updateCharacteristic(it)
}
} }
} }
fun campaignFlow(): StateFlow<Campaign> = flow private suspend fun update() {
_campaignFlow.value = load()
}
private suspend fun load(): Campaign { private suspend fun load(): Campaign {
val request: CampaignJson = client val request = client.campaign()
.get("http://pixelized.freeboxos.fr:16030/campaign") // TODO
.body()
val data = factory.convertFromJson(json = request) val data = factory.convertFromJson(json = request)
return data return data
} }
private fun updateCharacteristic( private fun updateCharacteristic(
message: UpdatePlayerCharacteristicMessage, characterId: String,
characteristic: Campaign.CharacterInstance.Characteristic,
value: Int,
) { ) {
val characters = flow.value.characters.toMutableMap() val campaign = _campaignFlow.value
val character = characters[message.characterId] ?: Campaign.CharacterInstance( val characters = campaign.characters.toMutableMap().also {
characteristic = emptyMap(), it[characterId] = useCase.updateCharacteristic(
usedSkill = emptyList(), character = campaign.character(id = characterId),
) characteristic = characteristic,
characters[message.characterId] = useCase.updateCharacteristic( value = value
character = character, )
characteristic = message.characteristic, }
value = message.value _campaignFlow.value = _campaignFlow.value.copy(characters = characters)
) }
flow.value = flow.value.copy(characters = characters)
private suspend fun handleMessage(message: Message) {
when (val payload = message.value) {
is RestSynchronisation.Campaign -> {
update()
}
is UpdatePlayerCharacteristicMessage -> {
updateCharacteristic(
characterId = payload.characterId,
characteristic = payload.characteristic,
value = payload.value,
)
}
else -> Unit
}
} }
} }

View file

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

View file

@ -1,68 +1,67 @@
package com.pixelized.desktop.lwa.repository.characterSheet package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class CharacterSheetRepository( class CharacterSheetRepository(
private val store: CharacterSheetStore, private val store: CharacterSheetStore,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO + Job()) private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets = store.characterSheetFlow() val characterSheetPreviewFlow get() = store.previewFlow
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
private val diminished = MutableStateFlow<Map<String, Int>>(emptyMap()) fun characterPreview(characterId: String?): CharacterSheetPreview? {
return characterSheetPreviewFlow.value.firstOrNull { it.id == characterId }
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> {
return sheets
} }
fun characterSheetFlow(id: String?): StateFlow<CharacterSheet?> { suspend fun characterDetail(
return sheets characterId: String?,
.map { sheets -> forceUpdate: Boolean = false,
sheets.firstOrNull { sheet -> sheet.id == id } ): CharacterSheet? {
} return try {
.stateIn( characterId?.let { store.characterDetail(characterId = it, forceUpdate = forceUpdate) }
scope = scope, } catch (exception: Exception) {
started = SharingStarted.Eagerly, null
initialValue = sheets.value.firstOrNull { it.id == id }
)
}
fun characterDiminishedFlow(id: String?): StateFlow<Int> {
return diminished
.map {
it[id] ?: 0
}
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = diminished.value[id] ?: 0
)
}
fun save(characterSheet: CharacterSheet) {
// store.save(sheet = characterSheet)
}
fun delete(id: String) {
// store.delete(id = id)
}
fun setDiminishedForCharacter(id: String, diminished: Int) {
this.diminished.value = this.diminished.value.toMutableMap().apply {
this[id] = diminished
} }
} }
fun characterDetailFlow(
characterId: String?,
forceUpdate: Boolean = false,
): StateFlow<CharacterSheet?> {
val initial = store.detailFlow.value[characterId]
if (forceUpdate || initial == null) {
scope.launch {
characterDetail(
characterId = characterId,
forceUpdate = forceUpdate,
)
}
}
return store.detailFlow
.map { sheets ->
sheets[characterId]
}
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = initial,
)
}
suspend fun updateCharacter(characterSheet: CharacterSheet) {
store.updateCharacter(sheet = characterSheet)
}
suspend fun deleteCharacter(characterId: String) {
store.deleteCharacter(characterId = characterId)
}
} }

View file

@ -1,77 +1,146 @@
package com.pixelized.desktop.lwa.repository.characterSheet 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.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.MessageType import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage import com.pixelized.shared.lwa.protocol.websocket.payload.RestSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage
import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase import com.pixelized.shared.lwa.usecase.CharacterSheetUseCase
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
class CharacterSheetStore( class CharacterSheetStore(
private val client: LwaClient,
private val network: NetworkRepository, private val network: NetworkRepository,
private val factory: CharacterSheetJsonFactory, private val factory: CharacterSheetJsonFactory,
private val useCase: CharacterSheetUseCase, private val useCase: CharacterSheetUseCase,
private val client: HttpClient,
private val json: Json,
) { ) {
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList()) private val _previewFlow = MutableStateFlow<List<CharacterSheetPreview>>(value = emptyList())
val previewFlow: StateFlow<List<CharacterSheetPreview>> get() = _previewFlow
private val _detailFlow = MutableStateFlow<Map<String, CharacterSheet>>(value = emptyMap())
val detailFlow: StateFlow<Map<String, CharacterSheet>> get() = _detailFlow
init { init {
val scope = CoroutineScope(Dispatchers.IO + Job()) val scope = CoroutineScope(Dispatchers.IO + Job())
// initial data loading.
scope.launch { scope.launch {
flow.value = load() _previewFlow.value = charactersPreview()
} }
// data update through WebSocket.
scope.launch { scope.launch {
network.data network.data.collect(::handleMessage)
.mapNotNull { it.takeIf { it.type == MessageType.UpdateSkillUsage } }
.map { json.decodeFromString<UpdateSkillUsageMessage>(it.value) }
.collect {
updateCharacterSkillChange(
characterId = it.characterId,
skillId = it.skillId,
)
}
} }
} }
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow // region Rest
suspend fun load(): List<CharacterSheet> { suspend fun charactersPreview(): List<CharacterSheetPreview> {
val request: List<CharacterSheetJson> = client val request = client.characters()
.get("http://pixelized.freeboxos.fr:16030/characters") // TODO
.body()
val data = request.map { val data = request.map {
factory.convertFromJson(json = it) CharacterSheetPreview(
id = it.id,
name = it.name,
level = it.level,
)
} }
return data return data
} }
private fun updateCharacterSkillChange( suspend fun characterDetail(
characterId: String,
forceUpdate: Boolean = false,
): CharacterSheet {
val character = _detailFlow.value[characterId]
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
}
}
suspend fun updateCharacter(
sheet: CharacterSheet,
) {
val json = factory.convertToJson(sheet = sheet)
try {
client.updateCharacter(sheet = json)
} catch (exception: Exception) {
}
_detailFlow.update(sheet = sheet)
}
suspend fun deleteCharacter(
characterId: String,
) {
try {
client.deleteCharacter(id = characterId)
} catch (exception: Exception) {
}
_detailFlow.delete(characterId = characterId)
}
// endregion
// region: WebSocket & data update.
private suspend fun handleMessage(message: Message) {
when (val payload = message.value) {
is RestSynchronisation.CharacterUpdate -> characterDetail(
characterId = payload.id,
forceUpdate = true
)
is RestSynchronisation.CharacterDelete -> {
_previewFlow.value = previewFlow.value.toMutableList()
.also { sheets -> sheets.removeIf { it.id == payload.characterId } }
_detailFlow.delete(payload.characterId)
}
is UpdateSkillUsageMessage -> {
updateCharacterSkillChange(
characterId = payload.characterId,
skillId = payload.skillId,
)
}
else -> Unit
}
}
private suspend fun updateCharacterSkillChange(
characterId: String, characterId: String,
skillId: String, skillId: String,
) { ) {
val characters = flow.value.toMutableList() val character = useCase.updateSkillUsage(
val index = characters.indexOfFirst { it.id == characterId } character = characterDetail(characterId = characterId),
skillId = skillId,
)
_detailFlow.update(character)
}
if (index > -1) { // endregion
characters[index] = useCase.updateSkillUsage(
character = characters[index], private fun MutableStateFlow<Map<String, CharacterSheet>>.update(sheet: CharacterSheet): CharacterSheet {
skillId = skillId, value = value.toMutableMap().also {
) it[sheet.id] = sheet
flow.value = characters }
return sheet
}
private fun MutableStateFlow<Map<String, CharacterSheet>>.delete(characterId: String) {
value = value.toMutableMap().also {
it.remove(characterId)
} }
} }
} }

View file

@ -4,13 +4,8 @@ import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame
import com.pixelized.desktop.lwa.utils.extention.encodeToFrame import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.shared.lwa.SERVER_PORT import com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.Message import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.MessagePayload
import com.pixelized.shared.lwa.protocol.payload.RollMessage
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -23,19 +18,12 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class NetworkRepository( class NetworkRepository(
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val client: HttpClient, private val client: HttpClient,
private val json: Json,
) { ) {
companion object {
const val DEFAULT_PORT = SERVER_PORT
const val DEFAULT_HOST = "pixelized.freeboxos.fr"
}
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
private var networkJob: Job? = null private var networkJob: Job? = null
@ -97,40 +85,11 @@ class NetworkRepository(
suspend fun share( suspend fun share(
playerName: String = settingsRepository.settings().playerName, playerName: String = settingsRepository.settings().playerName,
payload: MessagePayload, payload: MessagePayload,
) {
if (status.value == Status.CONNECTED) {
when (payload) {
is RollMessage -> share(
playerName = playerName,
type = MessageType.Roll,
content = json.encodeToString(payload),
)
is UpdateSkillUsageMessage -> share(
playerName = playerName,
type = MessageType.UpdateSkillUsage,
content = json.encodeToString(payload),
)
is UpdatePlayerCharacteristicMessage -> share(
playerName = playerName,
type = MessageType.UpdatePlayerCharacteristic,
content = json.encodeToString(payload),
)
}
}
}
suspend fun share(
playerName: String = settingsRepository.settings().playerName,
type: MessageType,
content: String,
) { ) {
if (status.value == Status.CONNECTED) { if (status.value == Status.CONNECTED) {
val message = Message( val message = Message(
from = playerName, from = playerName,
type = type, value = payload,
value = content,
) )
// emit the message into the outgoing buffer // emit the message into the outgoing buffer
outgoingMessageBuffer.emit(message) outgoingMessageBuffer.emit(message)

View file

@ -1,28 +1,22 @@
package com.pixelized.desktop.lwa.repository.roll_history package com.pixelized.desktop.lwa.repository.roll_history
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.shared.lwa.protocol.MessageType import com.pixelized.shared.lwa.protocol.websocket.payload.RollMessage
import com.pixelized.shared.lwa.protocol.payload.RollMessage
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class RollHistoryRepository( class RollHistoryRepository(
private val network: NetworkRepository, private val network: NetworkRepository,
private val jsonFormatter: Json,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO) private val scope = CoroutineScope(Dispatchers.IO)
val rolls: SharedFlow<RollMessage> = network.data val rolls: SharedFlow<RollMessage> = network.data
.mapNotNull { it.takeIf { it.type == MessageType.Roll } } .mapNotNull { it.value as? RollMessage }
.map { jsonFormatter.decodeFromString<RollMessage>(it.value) }
.shareIn( .shareIn(
scope = scope, scope = scope,
started = SharingStarted.Eagerly, started = SharingStarted.Eagerly,
@ -44,9 +38,6 @@ class RollHistoryRepository(
resultLabel = resultLabel, resultLabel = resultLabel,
rollSuccessLimit = rollSuccessLimit, rollSuccessLimit = rollSuccessLimit,
) )
network.share( network.share(payload = content)
type = MessageType.Roll,
content = jsonFormatter.encodeToString(content),
)
} }
} }

View file

@ -14,6 +14,8 @@ class SettingsFactory(
settings: Settings, settings: Settings,
): SettingsJson { ): SettingsJson {
return SettingsJsonV1( return SettingsJsonV1(
host = settings.host,
port = settings.port,
playerName = settings.playerName, playerName = settings.playerName,
) )
} }
@ -29,10 +31,11 @@ class SettingsFactory(
private fun convertFromJsonV1( private fun convertFromJsonV1(
json: SettingsJsonV1, json: SettingsJsonV1,
): Settings { ): Settings {
return with(useCase.defaultSettings()) { val default = useCase.defaultSettings()
Settings( return Settings(
playerName = json.playerName ?: playerName host = json.host ?: default.host,
) port = json.port ?: default.port,
} playerName = json.playerName ?: default.playerName
)
} }
} }

View file

@ -19,12 +19,14 @@ class SettingsStore(
private val useCase: SettingsUseCase, private val useCase: SettingsUseCase,
private val jsonFormatter: Json, private val jsonFormatter: Json,
) { ) {
private val settingsDirectory = File(storePath()).also { it.mkdirs() }
private val flow = MutableStateFlow(value = useCase.defaultSettings()) private val flow = MutableStateFlow(value = useCase.defaultSettings())
fun settingsFlow(): StateFlow<Settings> = flow fun settingsFlow(): StateFlow<Settings> = flow
init { init {
// create the directory
File(storePath()).also { it.mkdirs() }
// load the data.
val scope = CoroutineScope(Dispatchers.IO + Job()) val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch { scope.launch {
flow.value = load() flow.value = load()

View file

@ -1,5 +1,9 @@
package com.pixelized.desktop.lwa.repository.settings.model package com.pixelized.desktop.lwa.repository.settings.model
data class Settings( data class Settings(
val host: String,
val port: Int,
val playerName: String, val playerName: String,
) ) {
val root: String get() = "http://${host}:${port}"
}

View file

@ -4,5 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class SettingsJsonV1( data class SettingsJsonV1(
val host: String?,
val port: Int?,
val playerName: String?, val playerName: String?,
) : SettingsJson ) : SettingsJson

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player package com.pixelized.desktop.lwa.ui.screen.campaign
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column

View file

@ -90,7 +90,9 @@ fun CharacterDetail(
.width(width = 128.dp * 4), .width(width = 128.dp * 4),
character = it, character = it,
dynDetail = dynDetail, dynDetail = dynDetail,
onDismissRequest = { viewModel.hideCharacter() }, onDismissRequest = {
viewModel.hideCharacter()
},
onDiminished = { onDiminished = {
scope.launch { scope.launch {
dismissedViewModel.showDiminishedDialog(id = it.id) dismissedViewModel.showDiminishedDialog(id = it.id)
@ -108,7 +110,7 @@ fun CharacterDetail(
fun CharacterDetailContent( fun CharacterDetailContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
character: CharacterDetailUio, character: CharacterDetailUio,
dynDetail: State<CharacterDynDetailUio>, dynDetail: State<CharacterDynDetailUio?>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDiminished: () -> Unit, onDiminished: () -> Unit,
onHp: () -> Unit, onHp: () -> Unit,
@ -167,7 +169,7 @@ private fun Background(
private fun CharacterHeader( private fun CharacterHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
character: CharacterDetailUio, character: CharacterDetailUio,
dynDetail: State<CharacterDynDetailUio>, dynDetail: State<CharacterDynDetailUio?>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onDiminished: () -> Unit, onDiminished: () -> Unit,
onHp: () -> Unit, onHp: () -> Unit,
@ -222,7 +224,7 @@ private fun CharacterHeader(
style = MaterialTheme.typography.h6, style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.colorScheme.base.primary, color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
text = dynDetail.value.hp, text = dynDetail.value?.hp ?: character.hp,
) )
Text( Text(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),
@ -245,7 +247,7 @@ private fun CharacterHeader(
style = MaterialTheme.typography.h6, style = MaterialTheme.typography.h6,
color = MaterialTheme.lwa.colorScheme.base.primary, color = MaterialTheme.lwa.colorScheme.base.primary,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
text = dynDetail.value.pp, text = dynDetail.value?.pp ?: character.pp,
) )
Text( Text(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),

View file

@ -5,19 +5,22 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic.Damage import com.pixelized.shared.lwa.model.campaign.damage
import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic.Power import com.pixelized.shared.lwa.model.campaign.power
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.flow.map
class CharacterDetailViewModel( class CharacterDetailViewModel(
private val characterRepository: CharacterSheetRepository, private val characterRepository: CharacterSheetRepository,
@ -27,19 +30,14 @@ class CharacterDetailViewModel(
private val displayedCharacterId = MutableStateFlow<String?>(null) private val displayedCharacterId = MutableStateFlow<String?>(null)
val detail: StateFlow<CharacterDetailUio?> = combine( val detail: StateFlow<CharacterDetailUio?> = displayedCharacterId.map { id ->
displayedCharacterId, val sheet = characterRepository.characterDetail(id) ?: return@map null
characterRepository.characterSheetFlow(),
campaignRepository.campaignFlow(),
) { id, sheets, campaign ->
val sheet = sheets.firstOrNull { it.id == id }
if (sheet == null) return@combine null
CharacterDetailUio( CharacterDetailUio(
id = sheet.id, id = sheet.id,
portrait = sheet.portrait, portrait = sheet.portrait,
name = sheet.name, name = sheet.name,
hp = "${sheet.hp - (campaign.characters[id]?.characteristic?.get(Damage) ?: 0)}", hp = "${sheet.hp}",
pp = "${sheet.pp - (campaign.characters[id]?.characteristic?.get(Power) ?: 0)}", pp = "${sheet.pp}",
mov = "${sheet.movement}" mov = "${sheet.movement}"
) )
}.stateIn( }.stateIn(
@ -50,23 +48,25 @@ class CharacterDetailViewModel(
@Composable @Composable
@Stable @Stable
fun collectDynamicDetailAsState(id: String): State<CharacterDynDetailUio> { fun collectDynamicDetailAsState(id: String): State<CharacterDynDetailUio?> {
val flow = remember(id) { val scope = rememberCoroutineScope()
campaignRepository.characterInstance(id = id) val flow: StateFlow<CharacterDynDetailUio?> = remember(id) {
} combine(
return remember(id) { characterRepository.characterDetailFlow(id),
flow.mapNotNull { sheet -> campaignRepository.characterInstanceFlow(id = id),
) { sheet, instance ->
if (sheet == null) return@combine null
CharacterDynDetailUio( CharacterDynDetailUio(
hp = sheet.characteristic[Damage].toString(), hp = "${sheet.hp - instance.damage}",
pp = sheet.characteristic[Power].toString(), pp = "${sheet.power - instance.power}",
) )
} }.stateIn(
}.collectAsState( scope = scope,
CharacterDynDetailUio( started = SharingStarted.Eagerly,
hp = flow.value.characteristic[Damage].toString(), initialValue = null,
pp = flow.value.characteristic[Power].toString(),
) )
) }
return flow.collectAsState()
} }
fun showCharacter(id: String) { fun showCharacter(id: String) {

View file

@ -19,9 +19,10 @@ class CharacterDiminishedViewModel(
val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog
suspend fun showDiminishedDialog(id: String) { suspend fun showDiminishedDialog(id: String) {
val diminished = repository.characterDiminishedFlow(id = id).value val diminished = 0 // TODO repository.characterDiminishedFlow(id = id).value
val textFieldValue = val textFieldValue = mutableStateOf(
mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0))) TextFieldValue("$diminished", selection = TextRange(index = 0))
)
_diminishedDialog.value = DiminishedStatDialogUio( _diminishedDialog.value = DiminishedStatDialogUio(
id = id, id = id,
label = getString(resource = Res.string.character_sheet__diminished__label), label = getString(resource = Res.string.character_sheet__diminished__label),
@ -41,9 +42,10 @@ class CharacterDiminishedViewModel(
fun changeDiminished(dialog: DiminishedStatDialogUio) { fun changeDiminished(dialog: DiminishedStatDialogUio) {
val value = dialog.value().text.toIntOrNull() ?: 0 val value = dialog.value().text.toIntOrNull() ?: 0
repository.setDiminishedForCharacter( // TODO
id = dialog.id, // repository.setDiminishedForCharacter(
diminished = value, // id = dialog.id,
) // diminished = value,
// )
} }
} }

View file

@ -35,9 +35,9 @@ import org.jetbrains.compose.resources.painterResource
data class PlayerPortraitUio( data class PlayerPortraitUio(
val id: String, val id: String,
val portrait: String?, val portrait: String?,
val damage: Int, val hp: Int,
val maxHp: Int, val maxHp: Int,
val usedPp: Int, val pp: Int,
val maxPp: Int, val maxPp: Int,
) )
@ -96,7 +96,7 @@ fun PlayerPortrait(
Text( Text(
modifier = Modifier.padding(bottom = 2.dp), modifier = Modifier.padding(bottom = 2.dp),
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
text = "${character.maxHp - character.damage}/${character.maxHp}", text = "${character.hp}/${character.maxHp}",
) )
} }
Row( Row(
@ -111,7 +111,7 @@ fun PlayerPortrait(
Text( Text(
modifier = Modifier.padding(bottom = 2.dp), modifier = Modifier.padding(bottom = 2.dp),
style = MaterialTheme.typography.caption, style = MaterialTheme.typography.caption,
text = "${character.maxPp - character.usedPp}/${character.maxPp}", text = "${character.pp}/${character.maxPp}",
) )
} }
} }

View file

@ -46,6 +46,7 @@ import org.jetbrains.compose.resources.painterResource
data class PlayerPortraitRollUio( data class PlayerPortraitRollUio(
val characterId: String, val characterId: String,
val value: Int?, val value: Int?,
val label: String?,
) )
@Stable @Stable
@ -119,6 +120,12 @@ fun PlayerPortraitRoll(
color = MaterialTheme.colors.onSurface, color = MaterialTheme.colors.onSurface,
text = it.value.toString() text = it.value.toString()
) )
Text(
modifier = Modifier.padding(top = 84.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.caption,
text = it.label ?: "",
)
} }
} }
} }

View file

@ -0,0 +1,24 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.model.campaign.damage
import com.pixelized.shared.lwa.model.campaign.power
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
class PlayerRibbonFactory {
fun convertToPlayerPortraitUio(
characterSheet: CharacterSheet?,
characterInstance: Campaign.CharacterInstance,
): PlayerPortraitUio? {
if (characterSheet == null) return null
return PlayerPortraitUio(
id = characterSheet.id,
portrait = characterSheet.thumbnail,
hp = characterSheet.hp - characterInstance.damage,
maxHp = characterSheet.hp,
pp = characterSheet.pp - characterInstance.power,
maxPp = characterSheet.pp,
)
}
}

View file

@ -11,39 +11,31 @@ import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.shared.lwa.model.campaign.character
import com.pixelized.shared.lwa.model.campaign.damage
import com.pixelized.shared.lwa.model.campaign.power
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
class PlayerRibbonViewModel( class PlayerRibbonViewModel(
private val rollHistoryRepository: RollHistoryRepository, private val rollHistoryRepository: RollHistoryRepository,
characterRepository: CharacterSheetRepository, characterRepository: CharacterSheetRepository,
private val ribbonFactory: PlayerRibbonFactory,
campaignRepository: CampaignRepository, campaignRepository: CampaignRepository,
) : ViewModel() { ) : ViewModel() {
val characters: StateFlow<List<PlayerPortraitUio>> = combine(
characterRepository.characterSheetFlow(), val characters: StateFlow<List<PlayerPortraitUio>> = campaignRepository.campaignFlow
campaignRepository.campaignFlow(), .map { campaign ->
) { sheets, campaign -> campaign.characters.mapNotNull { entry ->
sheets.map { sheet -> ribbonFactory.convertToPlayerPortraitUio(
val instance = campaign.character(id = sheet.id) characterSheet = characterRepository.characterDetail(characterId = entry.key),
PlayerPortraitUio( characterInstance = entry.value,
id = sheet.id, )
portrait = sheet.thumbnail, }
damage = instance.damage, }.stateIn(
maxHp = sheet.hp, scope = viewModelScope,
usedPp = instance.power, started = SharingStarted.Eagerly,
maxPp = sheet.pp, initialValue = emptyList()
) )
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>() private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()
@ -52,11 +44,12 @@ class PlayerRibbonViewModel(
fun roll(characterId: String): State<PlayerPortraitRollUio?> { fun roll(characterId: String): State<PlayerPortraitRollUio?> {
val state = rolls.getOrPut(characterId) { mutableStateOf(null) } val state = rolls.getOrPut(characterId) { mutableStateOf(null) }
LaunchedEffect(characterId) { LaunchedEffect(characterId) {
rollHistoryRepository.rolls.collect { rollHistoryRepository.rolls.collect { roll ->
if (it.characterId == characterId) { if (roll.characterId == characterId) {
state.value = PlayerPortraitRollUio( state.value = PlayerPortraitRollUio(
characterId = characterId, characterId = characterId,
value = it.rollValue, value = roll.rollValue,
label = roll.resultLabel?.split(" ")?.joinToString(separator = "\n") { it }
) )
} }
} }

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
@ -17,21 +18,19 @@ import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheet
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialogUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.CharacterSheetDeleteConfirmationDialogUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.DiminishedStatDialogUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.StatChangeDialogUio import com.pixelized.desktop.lwa.ui.screen.characterSheet.detail.dialog.StatChangeDialogUio
import com.pixelized.desktop.lwa.utils.extention.collectAsState
import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic import com.pixelized.shared.lwa.model.campaign.Campaign.CharacterInstance.Characteristic
import com.pixelized.shared.lwa.model.campaign.damage import com.pixelized.shared.lwa.model.campaign.damage
import com.pixelized.shared.lwa.model.campaign.power import com.pixelized.shared.lwa.model.campaign.power
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage import com.pixelized.shared.lwa.protocol.websocket.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage import com.pixelized.shared.lwa.protocol.websocket.payload.UpdateSkillUsageMessage
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet__diminished__label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__hit_point
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sub_characteristics__power_point
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
@ -62,16 +61,18 @@ class CharacterSheetViewModel(
private val _diminishedDialog = mutableStateOf<DiminishedStatDialogUio?>(null) private val _diminishedDialog = mutableStateOf<DiminishedStatDialogUio?>(null)
val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog val diminishedDialog: State<DiminishedStatDialogUio?> get() = _diminishedDialog
private val diminishedValueFlow = characterRepository.characterDiminishedFlow(id = argument.id) // TODO
// private val diminishedValueFlow = characterRepository.characterDiminishedFlow(id = argument.id)
val diminishedValue: State<Int?> val diminishedValue: State<Int?>
@Composable @Composable
get() = diminishedValueFlow.collectAsState { it -> // get() = diminishedValueFlow.collectAsState { it ->
it.takeIf { it > 0 } // it.takeIf { it > 0 }
} // }
get() = remember { mutableStateOf(null) }
private val sheetFlow = combine( private val sheetFlow = combine(
characterRepository.characterSheetFlow(id = argument.id), characterRepository.characterDetailFlow(characterId = argument.id),
campaignRepository.campaignFlow(), campaignRepository.campaignFlow,
alteration.alterations(characterId = argument.id), alteration.alterations(characterId = argument.id),
transform = { sheet, campaign, alterations -> transform = { sheet, campaign, alterations ->
factory.convertToUio( factory.convertToUio(
@ -93,8 +94,8 @@ class CharacterSheetViewModel(
alteration.toggle(argument.id, "65e37d32-3031-4bf8-9369-d2c45d2efac0") alteration.toggle(argument.id, "65e37d32-3031-4bf8-9369-d2c45d2efac0")
} }
fun deleteCharacter(id: String) { suspend fun deleteCharacter(id: String) {
characterRepository.delete(id = id) characterRepository.deleteCharacter(characterId = id)
} }
fun onUseSkill(skill: CharacterSheetPageUio.Node) { fun onUseSkill(skill: CharacterSheetPageUio.Node) {
@ -109,10 +110,12 @@ class CharacterSheetViewModel(
} }
fun showConfirmCharacterDeletionDialog() { fun showConfirmCharacterDeletionDialog() {
characterRepository.characterSheetFlow(id = argument.id).value?.let { sheet -> characterRepository.characterPreview(
characterId = argument.id
)?.let { preview ->
_displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio( _displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio(
id = sheet.id, id = preview.id,
name = sheet.name, name = preview.name,
) )
} }
} }
@ -122,8 +125,10 @@ class CharacterSheetViewModel(
} }
suspend fun showSubCharacteristicDialog(id: String) { suspend fun showSubCharacteristicDialog(id: String) {
characterRepository.characterSheetFlow(id = argument.id).value?.let { sheet -> characterRepository.characterDetail(
val instance = campaignRepository.characterInstance(id = argument.id).value characterId = argument.id,
)?.let { sheet ->
val instance = campaignRepository.characterInstanceFlow(id = argument.id).value
_statChangeDialog.value = when (id) { _statChangeDialog.value = when (id) {
CharacterSheet.CharacteristicId.HP -> { CharacterSheet.CharacteristicId.HP -> {
val value = mutableStateOf( val value = mutableStateOf(
@ -169,8 +174,9 @@ class CharacterSheetViewModel(
value: Int, value: Int,
) { ) {
viewModelScope.launch { viewModelScope.launch {
val sheet = characterRepository.characterSheetFlow(id = argument.id).value characterRepository.characterDetail(
if (sheet != null) { characterId = argument.id,
)?.let { sheet ->
network.share( network.share(
payload = UpdatePlayerCharacteristicMessage( payload = UpdatePlayerCharacteristicMessage(
characterId = argument.id, characterId = argument.id,
@ -195,20 +201,20 @@ class CharacterSheetViewModel(
} }
suspend fun showDiminishedDialog() { suspend fun showDiminishedDialog() {
val diminished = characterRepository.characterDiminishedFlow(id = argument.id).value // val diminished = characterRepository.characterDiminishedFlow(id = argument.id).value
val textFieldValue = // val textFieldValue =
mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0))) // mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0)))
_diminishedDialog.value = DiminishedStatDialogUio( // _diminishedDialog.value = DiminishedStatDialogUio(
id = argument.id, // id = argument.id,
label = getString(resource = Res.string.character_sheet__diminished__label), // label = getString(resource = Res.string.character_sheet__diminished__label),
value = { textFieldValue.value }, // value = { textFieldValue.value },
onValueChange = { value -> // onValueChange = { value ->
textFieldValue.value = when (value.text.toIntOrNull()?.takeIf { it >= 0 }) { // textFieldValue.value = when (value.text.toIntOrNull()?.takeIf { it >= 0 }) {
null -> TextFieldValue("0", selection = TextRange(index = 0)) // null -> TextFieldValue("0", selection = TextRange(index = 0))
else -> value // else -> value
} // }
}, // },
) // )
} }
fun hideDiminishedDialog() { fun hideDiminishedDialog() {
@ -216,10 +222,10 @@ class CharacterSheetViewModel(
} }
fun changeDiminished(dialog: DiminishedStatDialogUio) { fun changeDiminished(dialog: DiminishedStatDialogUio) {
val value = dialog.value().text.toIntOrNull() ?: 0 // val value = dialog.value().text.toIntOrNull() ?: 0
characterRepository.setDiminishedForCharacter( // characterRepository.setDiminishedForCharacter(
id = dialog.id, // id = dialog.id,
diminished = value, // diminished = value,
) // )
} }
} }

View file

@ -27,13 +27,11 @@ class CharacterSheetEditViewModel(
private val argument = CharacterSheetEditDestination.Argument(savedStateHandle) private val argument = CharacterSheetEditDestination.Argument(savedStateHandle)
private val _characterSheet = mutableStateOf( private val _characterSheet = mutableStateOf(
characterSheetRepository.characterSheetFlow(id = argument.id).value.let { runBlocking {
runBlocking { sheetFactory.convertToUio(
sheetFactory.convertToUio( sheet = characterSheetRepository.characterDetail(characterId = argument.id),
sheet = it, onDeleteSkill = ::deleteSkill,
onDeleteSkill = ::deleteSkill, )
)
}
} }
) )
val characterSheet: State<CharacterSheetEditPageUio> get() = _characterSheet val characterSheet: State<CharacterSheetEditPageUio> get() = _characterSheet
@ -110,10 +108,10 @@ class CharacterSheetEditViewModel(
suspend fun save() { suspend fun save() {
val updatedSheet = sheetFactory.updateCharacterSheet( val updatedSheet = sheetFactory.updateCharacterSheet(
currentSheet = characterSheetRepository.characterSheetFlow(id = _characterSheet.value.id).value, currentSheet = characterSheetRepository.characterDetail(characterId = _characterSheet.value.id),
editedSheet = _characterSheet.value, editedSheet = _characterSheet.value,
) )
characterSheetRepository.save( characterSheetRepository.updateCharacter(
characterSheet = updatedSheet, characterSheet = updatedSheet,
) )
} }

View file

@ -11,21 +11,22 @@ import com.pixelized.shared.lwa.OperatingSystem
import com.pixelized.shared.lwa.storePath import com.pixelized.shared.lwa.storePath
class MainPageViewModel( class MainPageViewModel(
repository: CharacterSheetRepository, private val repository: CharacterSheetRepository,
networkRepository: NetworkRepository, networkRepository: NetworkRepository,
) : ViewModel() { ) : ViewModel() {
private val charactersFlow = repository.characterSheetFlow()
val characters: State<List<CharacterUio>> val characters: State<List<CharacterUio>>
@Composable @Composable
get() = charactersFlow.collectAsState { sheets -> get() = repository
sheets.map { sheet -> .characterSheetPreviewFlow
CharacterUio( .collectAsState { sheets ->
id = sheet.id, sheets.map { sheet ->
name = sheet.name, CharacterUio(
) id = sheet.id,
name = sheet.name,
)
}
} }
}
private val networkStatus = networkRepository.status private val networkStatus = networkRepository.status
val enableRollHistory: State<Boolean> val enableRollHistory: State<Boolean>
@ -33,7 +34,7 @@ class MainPageViewModel(
get() = networkStatus.collectAsState { it == NetworkRepository.Status.CONNECTED } get() = networkStatus.collectAsState { it == NetworkRepository.Status.CONNECTED }
fun openSaveDirectory( fun openSaveDirectory(
os: OperatingSystem = OperatingSystem.current os: OperatingSystem = OperatingSystem.current,
) { ) {
when (os) { when (os) {
OperatingSystem.Windows -> shellRun("explorer.exe", listOf(storePath(os = os))) OperatingSystem.Windows -> shellRun("explorer.exe", listOf(storePath(os = os)))

View file

@ -9,10 +9,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import com.pixelized.desktop.lwa.repository.network.NetworkRepository import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.ui.composable.blur.BlurContentController
import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackUio
import com.pixelized.desktop.lwa.utils.extention.collectAsState import com.pixelized.desktop.lwa.utils.extention.collectAsState
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
@ -23,8 +23,9 @@ class NetworkViewModel(
private val networkRepository: NetworkRepository, private val networkRepository: NetworkRepository,
private val factory: NetworkFactory, private val factory: NetworkFactory,
) : ViewModel() { ) : ViewModel() {
private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST) private val settings = settingsRepository.settings()
private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT) private val host = mutableStateOf(settings.host)
private val port = mutableStateOf(settings.port)
private val _networkError = MutableSharedFlow<ErrorSnackUio>() private val _networkError = MutableSharedFlow<ErrorSnackUio>()
val networkError: SharedFlow<ErrorSnackUio> get() = _networkError val networkError: SharedFlow<ErrorSnackUio> get() = _networkError
@ -64,7 +65,7 @@ class NetworkViewModel(
} }
fun onPortChange(port: String) { fun onPortChange(port: String) {
this.port.value = port.toIntOrNull() ?: 0 this.port.value = port.toIntOrNull() ?: settings.port
} }
fun onHostChange(host: String) { fun onHostChange(host: String) {
@ -75,6 +76,15 @@ class NetworkViewModel(
controller.show() controller.show()
_isLoading.value = true _isLoading.value = true
if (settings.host != host.value || settings.port != port.value) {
settingsRepository.update(
settings = settings.copy(
host = host.value,
port = port.value
)
)
}
networkRepository.connect( networkRepository.connect(
host = host.value, host = host.value,
port = port.value, port = port.value,

View file

@ -59,7 +59,7 @@ class RollViewModel(
sheet: CharacterSheetPageUio, sheet: CharacterSheetPageUio,
characteristic: CharacterSheetPageUio.Characteristic, characteristic: CharacterSheetPageUio.Characteristic,
) { ) {
val diminished = characterSheetRepository.characterDiminishedFlow(id = sheet.id).value val diminished = 0 // TODO characterSheetRepository.characterDiminishedFlow(id = sheet.id).value
prepareRoll( prepareRoll(
sheet = sheet, sheet = sheet,
label = characteristic.label, label = characteristic.label,
@ -98,12 +98,12 @@ class RollViewModel(
rollAction: String, rollAction: String,
rollSuccessValue: Int?, rollSuccessValue: Int?,
) { ) {
runBlocking { this.sheet = runBlocking {
rollRotation.snapTo(0f) rollRotation.snapTo(0f)
rollScale.snapTo(1f) rollScale.snapTo(1f)
characterSheetRepository.characterDetail(characterId = sheet.id)!!
} }
this.sheet = characterSheetRepository.characterSheetFlow(id = sheet.id).value!!
this.rollAction = rollAction this.rollAction = rollAction
this.rollSuccessValue = rollSuccessValue this.rollSuccessValue = rollSuccessValue

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
@ -21,15 +22,15 @@ class RollHistoryViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
combine( combine(
characterRepository.characterSheetFlow(), characterRepository.characterSheetPreviewFlow,
rollRepository.rolls, rollRepository.rolls,
) { sheets: List<CharacterSheet>, content -> ) { sheets: List<CharacterSheetPreview>, content ->
_rolls.value.toMutableList().apply { _rolls.value.toMutableList().apply {
val name = sheets.firstOrNull { it.id == content.characterId }?.name ?: ""
add( add(
index = 0, index = 0,
element = RollHistoryItemUio( element = RollHistoryItemUio(
character = sheets.firstOrNull { it.id == content.characterId }?.name character = name,
?: "",
skillLabel = content.skillLabel, skillLabel = content.skillLabel,
rollDifficulty = content.rollDifficulty, rollDifficulty = content.rollDifficulty,
resultLabel = content.resultLabel, resultLabel = content.resultLabel,

View file

@ -1,6 +1,6 @@
package com.pixelized.desktop.lwa.utils.extention package com.pixelized.desktop.lwa.utils.extention
import com.pixelized.shared.lwa.protocol.Message import com.pixelized.shared.lwa.protocol.websocket.Message
import io.ktor.websocket.Frame import io.ktor.websocket.Frame
import io.ktor.websocket.readText import io.ktor.websocket.readText
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View file

@ -1,8 +1,8 @@
[versions] [versions]
kotlin = "2.0.21" kotlin = "2.0.21"
kotlinx-coroutines = "1.9.0" kotlinx-coroutines = "1.9.0"
kotlinx-json = "1.7.3" kotlinx-json = "1.8.0"
compose-multiplatform = "1.7.0" compose-multiplatform = "1.7.3"
androidx-lifecycle = "2.8.3" androidx-lifecycle = "2.8.3"
androidx-navigation = "2.8.0-alpha10" androidx-navigation = "2.8.0-alpha10"
ktor = "3.0.1" ktor = "3.0.1"
@ -10,7 +10,6 @@ koin = "4.0.0"
turtle = "0.5.0" turtle = "0.5.0"
logback = "1.5.11" logback = "1.5.11"
coil = "3.1.0" coil = "3.1.0"
filament-android = "1.17.1"
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
@ -38,7 +37,6 @@ koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", ver
ktor-serialization-json = { group = 'io.ktor', name = 'ktor-serialization-kotlinx-json', version.ref = "ktor" } ktor-serialization-json = { group = 'io.ktor', name = 'ktor-serialization-kotlinx-json', version.ref = "ktor" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor" }
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" } ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor" } ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor" }
ktor-client-negotiation = { group = 'io.ktor', name = 'ktor-client-content-negotiation', version.ref = "ktor" } ktor-client-negotiation = { group = 'io.ktor', name = 'ktor-client-content-negotiation', version.ref = "ktor" }
@ -51,5 +49,4 @@ turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtl
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
filament-android = { group = "com.google.ar.sceneform", name = "filament-android", version.ref = "filament-android" }

View file

@ -22,5 +22,4 @@ dependencies {
implementation(libs.ktor.server.websockets) implementation(libs.ktor.server.websockets)
implementation(libs.ktor.server.negotiation) implementation(libs.ktor.server.negotiation)
implementation(libs.ktor.serialization.json) implementation(libs.ktor.serialization.json)
implementation(libs.filament.android)
} }

View file

@ -97,7 +97,7 @@ class CampaignService(
return true return true
} }
// Data manipulation threw WebSocket. // Data manipulation through WebSocket.
suspend fun updateCharacteristic( suspend fun updateCharacteristic(
characterId: String, characterId: String,

View file

@ -43,7 +43,7 @@ class CharacterSheetService(
return store.delete(id = characterId) return store.delete(id = characterId)
} }
// Data manipulation threw WebSocket. // Data manipulation through WebSocket.
fun updateCharacterLevel( fun updateCharacterLevel(
characterId: String, characterId: String,

View file

@ -17,9 +17,6 @@ class Engine(
suspend fun handle(message: Message) { suspend fun handle(message: Message) {
when (val data = message.value) { when (val data = message.value) {
RestSynchronisation.Campaign -> Unit // TODO
is RestSynchronisation.CharacterUpdate -> Unit // TODO
is RollMessage -> Unit // Nothing to do here. is RollMessage -> Unit // Nothing to do here.
@ -34,9 +31,11 @@ class Engine(
skillId = data.skillId skillId = data.skillId
) )
is RestSynchronisation.CharacterDelete -> characterService.deleteCharacter( RestSynchronisation.Campaign -> Unit // Handle in the Rest
characterId = data.characterId,
) is RestSynchronisation.CharacterUpdate -> Unit // Handle in the Rest
is RestSynchronisation.CharacterDelete -> Unit // Handle in the Rest
} }
} }
} }

View file

@ -72,6 +72,33 @@ class LocalServer {
} }
routing { routing {
webSocket(
path = "/ws",
handler = {
val job = launch {
// send local message to the clients
engine.webSocket.collect { message ->
send(json.encodeToFrame(message))
}
}
runCatching {
// watching for clients incoming message
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val message = Json.decodeFromFrame(frame = frame)
// log the message
engine.handle(message)
// broadcast to clients the message
engine.webSocket.emit(message)
}
}
}.onFailure { exception ->
println("WebSocket exception: ${exception.localizedMessage}")
}.also {
job.cancel()
}
}
)
get( get(
path = "/characters", path = "/characters",
body = engine.getCharacters(), body = engine.getCharacters(),
@ -116,33 +143,6 @@ class LocalServer {
) )
} }
} }
webSocket(
path = "/ws",
handler = {
val job = launch {
// send local message to the clients
engine.webSocket.collect { message ->
send(json.encodeToFrame(message))
}
}
runCatching {
// watching for clients incoming message
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val message = Json.decodeFromFrame(frame = frame)
// log the message
engine.handle(message)
// broadcast to clients the message
engine.webSocket.emit(message)
}
}
}.onFailure { exception ->
println("WebSocket exception: ${exception.localizedMessage}")
}.also {
job.cancel()
}
}
)
} }
} }
) )
@ -154,7 +154,6 @@ class LocalServer {
try { try {
server?.start(wait = true) server?.start(wait = true)
} catch (exception: Exception) { } catch (exception: Exception) {
// TODO
println("WebSocket exception: ${exception.localizedMessage}") println("WebSocket exception: ${exception.localizedMessage}")
} finally { } finally {
println("Server close") println("Server close")