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 {
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}

View file

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

View file

@ -14,4 +14,7 @@
# Serialization
-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.window.ApplicationScope
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.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.CharacterSheetEditDestination
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.RollHistoryWindow
import com.pixelized.desktop.lwa.ui.navigation.window.rememberMaxWindowHeight
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
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.campaign.CampaignScreen
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.theme.LwaTheme
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.ui.tooling.preview.Preview
import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel
val LocalWindowController = compositionLocalOf<WindowController> {
error("Local Window Controller is not yet ready")
@ -127,8 +125,8 @@ fun ApplicationScope.App() {
}
},
content = {
MainNavHost()
// CampaignScreen()
// MainNavHost()
CampaignScreen()
}
)
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.SettingsUseCase
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.expression.ExpressionParser
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.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.ribbon.PlayerRibbonFactory
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.CharacterSheetViewModel
@ -72,6 +75,7 @@ val toolsDependencies
}
}
}
single<LwaClient> { LwaClientImpl(get(), get()) }
}
val storeDependencies
@ -100,6 +104,7 @@ val factoryDependencies
factoryOf(::SkillFieldFactory)
factoryOf(::SettingsFactory)
factoryOf(::CampaignJsonFactory)
factoryOf(::PlayerRibbonFactory)
}
val viewModelDependencies

View file

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

View file

@ -1,72 +1,82 @@
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.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.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.UpdatePlayerCharacteristicMessage
import com.pixelized.shared.lwa.model.campaign.character
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 io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
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.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlin.collections.set
class CampaignStore(
private val client: LwaClient,
private val network: NetworkRepository,
private val factory: CampaignJsonFactory,
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 {
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
flow.value = load()
update()
}
scope.launch {
network.data
.mapNotNull { it.takeIf { it.type == MessageType.UpdatePlayerCharacteristic } }
.map { json.decodeFromString<UpdatePlayerCharacteristicMessage>(it.value) }
.collect {
updateCharacteristic(it)
}
network.data.collect(::handleMessage)
}
}
fun campaignFlow(): StateFlow<Campaign> = flow
private suspend fun update() {
_campaignFlow.value = load()
}
private suspend fun load(): Campaign {
val request: CampaignJson = client
.get("http://pixelized.freeboxos.fr:16030/campaign") // TODO
.body()
val request = client.campaign()
val data = factory.convertFromJson(json = request)
return data
}
private fun updateCharacteristic(
message: UpdatePlayerCharacteristicMessage,
characterId: String,
characteristic: Campaign.CharacterInstance.Characteristic,
value: Int,
) {
val characters = flow.value.characters.toMutableMap()
val character = characters[message.characterId] ?: Campaign.CharacterInstance(
characteristic = emptyMap(),
usedSkill = emptyList(),
)
characters[message.characterId] = useCase.updateCharacteristic(
character = character,
characteristic = message.characteristic,
value = message.value
)
flow.value = flow.value.copy(characters = characters)
val campaign = _campaignFlow.value
val characters = campaign.characters.toMutableMap().also {
it[characterId] = useCase.updateCharacteristic(
character = campaign.character(id = characterId),
characteristic = characteristic,
value = value
)
}
_campaignFlow.value = _campaignFlow.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
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class CharacterSheetRepository(
private val store: CharacterSheetStore,
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
private val sheets = store.characterSheetFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
val characterSheetPreviewFlow get() = store.previewFlow
private val diminished = MutableStateFlow<Map<String, Int>>(emptyMap())
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> {
return sheets
fun characterPreview(characterId: String?): CharacterSheetPreview? {
return characterSheetPreviewFlow.value.firstOrNull { it.id == characterId }
}
fun characterSheetFlow(id: String?): StateFlow<CharacterSheet?> {
return sheets
.map { sheets ->
sheets.firstOrNull { sheet -> sheet.id == id }
}
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
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
suspend fun characterDetail(
characterId: String?,
forceUpdate: Boolean = false,
): CharacterSheet? {
return try {
characterId?.let { store.characterDetail(characterId = it, forceUpdate = forceUpdate) }
} catch (exception: Exception) {
null
}
}
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
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.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJsonFactory
import com.pixelized.shared.lwa.protocol.MessageType
import com.pixelized.shared.lwa.protocol.payload.UpdateSkillUsageMessage
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
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.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
class CharacterSheetStore(
private val client: LwaClient,
private val network: NetworkRepository,
private val factory: CharacterSheetJsonFactory,
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 {
val scope = CoroutineScope(Dispatchers.IO + Job())
// initial data loading.
scope.launch {
flow.value = load()
_previewFlow.value = charactersPreview()
}
// data update through WebSocket.
scope.launch {
network.data
.mapNotNull { it.takeIf { it.type == MessageType.UpdateSkillUsage } }
.map { json.decodeFromString<UpdateSkillUsageMessage>(it.value) }
.collect {
updateCharacterSkillChange(
characterId = it.characterId,
skillId = it.skillId,
)
}
network.data.collect(::handleMessage)
}
}
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow
// region Rest
suspend fun load(): List<CharacterSheet> {
val request: List<CharacterSheetJson> = client
.get("http://pixelized.freeboxos.fr:16030/characters") // TODO
.body()
suspend fun charactersPreview(): List<CharacterSheetPreview> {
val request = client.characters()
val data = request.map {
factory.convertFromJson(json = it)
CharacterSheetPreview(
id = it.id,
name = it.name,
level = it.level,
)
}
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,
skillId: String,
) {
val characters = flow.value.toMutableList()
val index = characters.indexOfFirst { it.id == characterId }
val character = useCase.updateSkillUsage(
character = characterDetail(characterId = characterId),
skillId = skillId,
)
_detailFlow.update(character)
}
if (index > -1) {
characters[index] = useCase.updateSkillUsage(
character = characters[index],
skillId = skillId,
)
flow.value = characters
// endregion
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) {
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.utils.extention.decodeFromFrame
import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.shared.lwa.SERVER_PORT
import com.pixelized.shared.lwa.protocol.Message
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 com.pixelized.shared.lwa.protocol.websocket.Message
import com.pixelized.shared.lwa.protocol.websocket.payload.MessagePayload
import io.ktor.client.HttpClient
import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope
@ -23,19 +18,12 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
class NetworkRepository(
private val settingsRepository: SettingsRepository,
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 var networkJob: Job? = null
@ -97,40 +85,11 @@ class NetworkRepository(
suspend fun share(
playerName: String = settingsRepository.settings().playerName,
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) {
val message = Message(
from = playerName,
type = type,
value = content,
value = payload,
)
// emit the message into the outgoing buffer
outgoingMessageBuffer.emit(message)

View file

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

View file

@ -14,6 +14,8 @@ class SettingsFactory(
settings: Settings,
): SettingsJson {
return SettingsJsonV1(
host = settings.host,
port = settings.port,
playerName = settings.playerName,
)
}
@ -29,10 +31,11 @@ class SettingsFactory(
private fun convertFromJsonV1(
json: SettingsJsonV1,
): Settings {
return with(useCase.defaultSettings()) {
Settings(
playerName = json.playerName ?: playerName
)
}
val default = useCase.defaultSettings()
return Settings(
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 jsonFormatter: Json,
) {
private val settingsDirectory = File(storePath()).also { it.mkdirs() }
private val flow = MutableStateFlow(value = useCase.defaultSettings())
fun settingsFlow(): StateFlow<Settings> = flow
init {
// create the directory
File(storePath()).also { it.mkdirs() }
// load the data.
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
flow.value = load()

View file

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

View file

@ -4,5 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class SettingsJsonV1(
val host: String?,
val port: Int?,
val playerName: String?,
) : 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.Column

View file

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

View file

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

View file

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

View file

@ -35,9 +35,9 @@ import org.jetbrains.compose.resources.painterResource
data class PlayerPortraitUio(
val id: String,
val portrait: String?,
val damage: Int,
val hp: Int,
val maxHp: Int,
val usedPp: Int,
val pp: Int,
val maxPp: Int,
)
@ -96,7 +96,7 @@ fun PlayerPortrait(
Text(
modifier = Modifier.padding(bottom = 2.dp),
style = MaterialTheme.typography.caption,
text = "${character.maxHp - character.damage}/${character.maxHp}",
text = "${character.hp}/${character.maxHp}",
)
}
Row(
@ -111,7 +111,7 @@ fun PlayerPortrait(
Text(
modifier = Modifier.padding(bottom = 2.dp),
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(
val characterId: String,
val value: Int?,
val label: String?,
)
@Stable
@ -119,6 +120,12 @@ fun PlayerPortraitRoll(
color = MaterialTheme.colors.onSurface,
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.characterSheet.CharacterSheetRepository
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.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class PlayerRibbonViewModel(
private val rollHistoryRepository: RollHistoryRepository,
characterRepository: CharacterSheetRepository,
private val ribbonFactory: PlayerRibbonFactory,
campaignRepository: CampaignRepository,
) : ViewModel() {
val characters: StateFlow<List<PlayerPortraitUio>> = combine(
characterRepository.characterSheetFlow(),
campaignRepository.campaignFlow(),
) { sheets, campaign ->
sheets.map { sheet ->
val instance = campaign.character(id = sheet.id)
PlayerPortraitUio(
id = sheet.id,
portrait = sheet.thumbnail,
damage = instance.damage,
maxHp = sheet.hp,
usedPp = instance.power,
maxPp = sheet.pp,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
val characters: StateFlow<List<PlayerPortraitUio>> = campaignRepository.campaignFlow
.map { campaign ->
campaign.characters.mapNotNull { entry ->
ribbonFactory.convertToPlayerPortraitUio(
characterSheet = characterRepository.characterDetail(characterId = entry.key),
characterInstance = entry.value,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
private val rolls = hashMapOf<String, MutableState<PlayerPortraitRollUio?>>()
@ -52,11 +44,12 @@ class PlayerRibbonViewModel(
fun roll(characterId: String): State<PlayerPortraitRollUio?> {
val state = rolls.getOrPut(characterId) { mutableStateOf(null) }
LaunchedEffect(characterId) {
rollHistoryRepository.rolls.collect {
if (it.characterId == characterId) {
rollHistoryRepository.rolls.collect { roll ->
if (roll.characterId == characterId) {
state.value = PlayerPortraitRollUio(
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.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
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.DiminishedStatDialogUio
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.damage
import com.pixelized.shared.lwa.model.campaign.power
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.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
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__power_point
import org.jetbrains.compose.resources.getString
@ -62,16 +61,18 @@ class CharacterSheetViewModel(
private val _diminishedDialog = mutableStateOf<DiminishedStatDialogUio?>(null)
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?>
@Composable
get() = diminishedValueFlow.collectAsState { it ->
it.takeIf { it > 0 }
}
// get() = diminishedValueFlow.collectAsState { it ->
// it.takeIf { it > 0 }
// }
get() = remember { mutableStateOf(null) }
private val sheetFlow = combine(
characterRepository.characterSheetFlow(id = argument.id),
campaignRepository.campaignFlow(),
characterRepository.characterDetailFlow(characterId = argument.id),
campaignRepository.campaignFlow,
alteration.alterations(characterId = argument.id),
transform = { sheet, campaign, alterations ->
factory.convertToUio(
@ -93,8 +94,8 @@ class CharacterSheetViewModel(
alteration.toggle(argument.id, "65e37d32-3031-4bf8-9369-d2c45d2efac0")
}
fun deleteCharacter(id: String) {
characterRepository.delete(id = id)
suspend fun deleteCharacter(id: String) {
characterRepository.deleteCharacter(characterId = id)
}
fun onUseSkill(skill: CharacterSheetPageUio.Node) {
@ -109,10 +110,12 @@ class CharacterSheetViewModel(
}
fun showConfirmCharacterDeletionDialog() {
characterRepository.characterSheetFlow(id = argument.id).value?.let { sheet ->
characterRepository.characterPreview(
characterId = argument.id
)?.let { preview ->
_displayDeleteConfirmationDialog.value = CharacterSheetDeleteConfirmationDialogUio(
id = sheet.id,
name = sheet.name,
id = preview.id,
name = preview.name,
)
}
}
@ -122,8 +125,10 @@ class CharacterSheetViewModel(
}
suspend fun showSubCharacteristicDialog(id: String) {
characterRepository.characterSheetFlow(id = argument.id).value?.let { sheet ->
val instance = campaignRepository.characterInstance(id = argument.id).value
characterRepository.characterDetail(
characterId = argument.id,
)?.let { sheet ->
val instance = campaignRepository.characterInstanceFlow(id = argument.id).value
_statChangeDialog.value = when (id) {
CharacterSheet.CharacteristicId.HP -> {
val value = mutableStateOf(
@ -169,8 +174,9 @@ class CharacterSheetViewModel(
value: Int,
) {
viewModelScope.launch {
val sheet = characterRepository.characterSheetFlow(id = argument.id).value
if (sheet != null) {
characterRepository.characterDetail(
characterId = argument.id,
)?.let { sheet ->
network.share(
payload = UpdatePlayerCharacteristicMessage(
characterId = argument.id,
@ -195,20 +201,20 @@ class CharacterSheetViewModel(
}
suspend fun showDiminishedDialog() {
val diminished = characterRepository.characterDiminishedFlow(id = argument.id).value
val textFieldValue =
mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0)))
_diminishedDialog.value = DiminishedStatDialogUio(
id = argument.id,
label = getString(resource = Res.string.character_sheet__diminished__label),
value = { textFieldValue.value },
onValueChange = { value ->
textFieldValue.value = when (value.text.toIntOrNull()?.takeIf { it >= 0 }) {
null -> TextFieldValue("0", selection = TextRange(index = 0))
else -> value
}
},
)
// val diminished = characterRepository.characterDiminishedFlow(id = argument.id).value
// val textFieldValue =
// mutableStateOf(TextFieldValue("$diminished", selection = TextRange(index = 0)))
// _diminishedDialog.value = DiminishedStatDialogUio(
// id = argument.id,
// label = getString(resource = Res.string.character_sheet__diminished__label),
// value = { textFieldValue.value },
// onValueChange = { value ->
// textFieldValue.value = when (value.text.toIntOrNull()?.takeIf { it >= 0 }) {
// null -> TextFieldValue("0", selection = TextRange(index = 0))
// else -> value
// }
// },
// )
}
fun hideDiminishedDialog() {
@ -216,10 +222,10 @@ class CharacterSheetViewModel(
}
fun changeDiminished(dialog: DiminishedStatDialogUio) {
val value = dialog.value().text.toIntOrNull() ?: 0
characterRepository.setDiminishedForCharacter(
id = dialog.id,
diminished = value,
)
// val value = dialog.value().text.toIntOrNull() ?: 0
// characterRepository.setDiminishedForCharacter(
// id = dialog.id,
// diminished = value,
// )
}
}

View file

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

View file

@ -11,21 +11,22 @@ import com.pixelized.shared.lwa.OperatingSystem
import com.pixelized.shared.lwa.storePath
class MainPageViewModel(
repository: CharacterSheetRepository,
private val repository: CharacterSheetRepository,
networkRepository: NetworkRepository,
) : ViewModel() {
private val charactersFlow = repository.characterSheetFlow()
val characters: State<List<CharacterUio>>
@Composable
get() = charactersFlow.collectAsState { sheets ->
sheets.map { sheet ->
CharacterUio(
id = sheet.id,
name = sheet.name,
)
get() = repository
.characterSheetPreviewFlow
.collectAsState { sheets ->
sheets.map { sheet ->
CharacterUio(
id = sheet.id,
name = sheet.name,
)
}
}
}
private val networkStatus = networkRepository.status
val enableRollHistory: State<Boolean>
@ -33,7 +34,7 @@ class MainPageViewModel(
get() = networkStatus.collectAsState { it == NetworkRepository.Status.CONNECTED }
fun openSaveDirectory(
os: OperatingSystem = OperatingSystem.current
os: OperatingSystem = OperatingSystem.current,
) {
when (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.lifecycle.ViewModel
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.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 kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
@ -23,8 +23,9 @@ class NetworkViewModel(
private val networkRepository: NetworkRepository,
private val factory: NetworkFactory,
) : ViewModel() {
private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST)
private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT)
private val settings = settingsRepository.settings()
private val host = mutableStateOf(settings.host)
private val port = mutableStateOf(settings.port)
private val _networkError = MutableSharedFlow<ErrorSnackUio>()
val networkError: SharedFlow<ErrorSnackUio> get() = _networkError
@ -64,7 +65,7 @@ class NetworkViewModel(
}
fun onPortChange(port: String) {
this.port.value = port.toIntOrNull() ?: 0
this.port.value = port.toIntOrNull() ?: settings.port
}
fun onHostChange(host: String) {
@ -75,6 +76,15 @@ class NetworkViewModel(
controller.show()
_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(
host = host.value,
port = port.value,

View file

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

View file

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

View file

@ -1,6 +1,6 @@
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.readText
import kotlinx.serialization.json.Json

View file

@ -1,8 +1,8 @@
[versions]
kotlin = "2.0.21"
kotlinx-coroutines = "1.9.0"
kotlinx-json = "1.7.3"
compose-multiplatform = "1.7.0"
kotlinx-json = "1.8.0"
compose-multiplatform = "1.7.3"
androidx-lifecycle = "2.8.3"
androidx-navigation = "2.8.0-alpha10"
ktor = "3.0.1"
@ -10,7 +10,6 @@ koin = "4.0.0"
turtle = "0.5.0"
logback = "1.5.11"
coil = "3.1.0"
filament-android = "1.17.1"
[plugins]
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-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-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" }
@ -51,5 +49,4 @@ turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtl
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
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" }
filament-android = { group = "com.google.ar.sceneform", name = "filament-android", version.ref = "filament-android" }
coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }

View file

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

View file

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

View file

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

View file

@ -17,9 +17,6 @@ class Engine(
suspend fun handle(message: Message) {
when (val data = message.value) {
RestSynchronisation.Campaign -> Unit // TODO
is RestSynchronisation.CharacterUpdate -> Unit // TODO
is RollMessage -> Unit // Nothing to do here.
@ -34,9 +31,11 @@ class Engine(
skillId = data.skillId
)
is RestSynchronisation.CharacterDelete -> characterService.deleteCharacter(
characterId = data.characterId,
)
RestSynchronisation.Campaign -> Unit // Handle in the Rest
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 {
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(
path = "/characters",
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 {
server?.start(wait = true)
} catch (exception: Exception) {
// TODO
println("WebSocket exception: ${exception.localizedMessage}")
} finally {
println("Server close")