Add basic network feature.

Basic implementation of a WebSocket protocol with JSON.

Note : obfuscation & release build are deactivated again. Netty (needed for Ktor server) is a nightmare to build in release with or without proguard.
Spend more time until now on project configuration that in actual prototyping.
This commit is contained in:
Thomas Andres Gomez 2024-11-08 18:58:47 +01:00
parent ba0cc30a1a
commit 0e5fee6771
22 changed files with 958 additions and 161 deletions

View file

@ -5,6 +5,8 @@ plugins {
alias(libs.plugins.kotlinSerialization) alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler) alias(libs.plugins.composeCompiler)
// kotlin("jvm") version "1.9.20"
// alias(libs.plugins.kotlinKtor)
} }
kotlin { kotlin {
@ -24,6 +26,12 @@ kotlin {
implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.websockets)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.websockets)
} }
commonTest.dependencies { commonTest.dependencies {
@ -53,12 +61,12 @@ compose.desktop {
includeAllModules = true includeAllModules = true
// Use system theming fot the app toolbars. // Use system theming for the app toolbars.
jvmArgs("-Dapple.awt.application.appearance=system") jvmArgs("-Dapple.awt.application.appearance=system")
} }
buildTypes.release.proguard { buildTypes.release.proguard {
obfuscate.set(true) // Obfuscation crash at runtime when try to use datastore. obfuscate.set(false) // Obfuscation doesn't work because of netty.
configurationFiles.from(project.file("compose-desktop.pro")) configurationFiles.from(project.file("compose-desktop.pro"))
} }
} }

View file

@ -1,5 +1,3 @@
## Data Store old dependancies not removed properly.
-dontwarn okio.AsyncTimeout$Watchdog
-keep class androidx.compose.runtime.** { *; } -keep class androidx.compose.runtime.** { *; }
-keep class androidx.collection.** { *; } -keep class androidx.collection.** { *; }

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<resources> <resources>
<string name="main_page__create_action">Créer une feuille de personnage</string> <string name="main_page__create_action">Créer une feuille de personnage</string>
<string name="main_page__network_action">Configuration réseau</string>
<string name="roll_page__critical_success">Réussite critique</string> <string name="roll_page__critical_success">Réussite critique</string>
<string name="roll_page__special_success">Réussite spéciale</string> <string name="roll_page__special_success">Réussite spéciale</string>
@ -65,4 +66,19 @@
<string name="character_sheet__skills__title">Compétences</string> <string name="character_sheet__skills__title">Compétences</string>
<string name="character_sheet__occupations_title">Occupations</string> <string name="character_sheet__occupations_title">Occupations</string>
<string name="character_sheet__magics__title">Compétences magiques</string> <string name="character_sheet__magics__title">Compétences magiques</string>
<string name="network__title">Configuration réseau</string>
<string name="network__player_name__label">Nom du joueur</string>
<string name="network__host__label">host</string>
<string name="network__port__label">port</string>
<string name="network__socket__connect_action">Se connecter à la table</string>
<string name="network__socket__host_action">Héberger la table</string>
<string name="network__socket__disconnect_action">Se déconnecter</string>
<string name="network__socket__status_state">État de connexion : %1$s</string>
<string name="network__socket__status_connected">Connecté</string>
<string name="network__socket__status_disconnected">Disconnecté</string>
<string name="network__socket__type_state">Type de connexion : %1$s</string>
<string name="network__socket__type_server">Serveur</string>
<string name="network__socket__type_client">Client</string>
<string name="network__socket__type_none">Aucun</string>
</resources> </resources>

View file

@ -3,19 +3,58 @@ package com.pixelized.desktop.lwa
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState
import com.pixelized.desktop.lwa.navigation.MainNavHost import com.pixelized.desktop.lwa.navigation.MainNavHost
import com.pixelized.desktop.lwa.theme.LwaTheme import com.pixelized.desktop.lwa.theme.LwaTheme
import org.jetbrains.compose.ui.tooling.preview.Preview import org.jetbrains.compose.ui.tooling.preview.Preview
val LocalWindowController = compositionLocalOf<WindowController> {
error("Local Window Controller is not yet ready")
}
@Stable
data class WindowController(
private val onCloseRequest: () -> Unit
) {
fun close() = onCloseRequest()
}
@Composable @Composable
@Preview @Preview
fun App() { fun ApplicationScope.App() {
LwaTheme { val controller = remember {
Surface( WindowController(
modifier = Modifier.fillMaxSize() onCloseRequest = ::exitApplication
)
}
CompositionLocalProvider(
LocalWindowController provides controller,
) {
Window(
onCloseRequest = {
controller.close()
},
state = rememberWindowState(
width = 320.dp + 64.dp,
height = 900.dp,
),
title = "LwaCharacterSheet",
) { ) {
MainNavHost() LwaTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
MainNavHost()
}
}
} }
} }
} }

View file

@ -7,11 +7,10 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.navigation.destination.MainDestination import com.pixelized.desktop.lwa.navigation.destination.MainDestination
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetEditPage
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetPage
import com.pixelized.desktop.lwa.navigation.destination.composableMainPage import com.pixelized.desktop.lwa.navigation.destination.composableMainPage
import com.pixelized.desktop.lwa.navigation.destination.composableNetworkPage
val LocalScreen = compositionLocalOf<NavHostController> { val LocalScreenController = compositionLocalOf<NavHostController> {
error("MainNavHost controller is not yet ready") error("MainNavHost controller is not yet ready")
} }
@ -21,16 +20,14 @@ fun MainNavHost(
startDestination: String = MainDestination.navigationRoute(), startDestination: String = MainDestination.navigationRoute(),
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalScreen provides controller, LocalScreenController provides controller,
) { ) {
NavHost( NavHost(
navController = controller, navController = controller,
startDestination = startDestination, startDestination = startDestination,
) { ) {
composableMainPage() composableMainPage()
composableNetworkPage()
composableCharacterSheetPage()
composableCharacterSheetEditPage()
} }
} }
} }

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.screen.network.NetworkPage
object NetworkDestination {
private const val ROUTE = "network"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableNetworkPage() {
composable(
route = NetworkDestination.baseRoute(),
) {
NetworkPage()
}
}
fun NavHostController.navigateToNetwork() {
val route = NetworkDestination.navigationRoute()
navigate(route = route)
}

View file

@ -0,0 +1,170 @@
package com.pixelized.desktop.lwa.repository.network
import com.pixelized.desktop.lwa.repository.network.helper.client
import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket
import com.pixelized.desktop.lwa.repository.network.helper.server
import com.pixelized.desktop.lwa.repository.network.protocol.Message
import io.ktor.client.HttpClient
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>
typealias Client = HttpClient
object NetworkRepository {
private val scope = CoroutineScope(Dispatchers.IO)
private var networkJob: Job? = null
private var server: Server? = null
private var client: Client? = null
private val messageResponseFlow = MutableSharedFlow<String>()
private val sharedFlow = messageResponseFlow.asSharedFlow()
private val _player = MutableStateFlow("")
val player: StateFlow<String> get() = _player
private val _status = MutableStateFlow(Status.DISCONNECTED)
val status: StateFlow<Status> get() = _status
private val _type = MutableStateFlow(Type.NONE)
val type: StateFlow<Type> get() = _type
fun onPlayerNameChange(player: String) {
_player.value = player
}
fun host(
port: Int,
) {
_type.value = Type.SERVER
_status.value = Status.CONNECTED
server = server(port = port) {
println("Server launched")
val job = launch {
sharedFlow.collect { message ->
println("Broadcast: $message")
send(Frame.Text(message))
}
}
runCatching {
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val receivedText = frame.readText()
messageResponseFlow.emit(receivedText)
}
}
}.onFailure { exception ->
// TODO
println("WebSocket exception: ${exception.localizedMessage}")
}.also {
job.cancel()
}
}
networkJob?.cancel()
networkJob = scope.launch {
try {
server?.start(wait = true)
} catch (exception: Exception) {
// TODO
println("WebSocket exception: ${exception.localizedMessage}")
} finally {
println("Server close")
_type.value = Type.NONE
_status.value = Status.DISCONNECTED
}
}
}
fun connect(
host: String,
port: Int,
) {
client = client()
networkJob?.cancel()
networkJob = scope.launch {
try {
client?.connectWebSocket(host = host, port = port) {
_type.value = Type.CLIENT
_status.value = Status.CONNECTED
println("Client launched")
val job = launch {
sharedFlow.collect { message ->
println("Send: $message")
send(Frame.Text(message))
}
}
runBlocking {
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val receivedText = frame.readText()
println("client received: $receivedText")
}
}
}.also {
job.cancel()
}
}
} catch (exception: Exception) {
// TODO
println("WebSocket exception: ${exception.localizedMessage}")
} finally {
println("Client close")
_type.value = Type.NONE
_status.value = Status.DISCONNECTED
}
}
}
fun disconnect() {
networkJob?.cancel()
scope.launch {
server?.stop()
client?.close()
}
}
fun share(
type: String,
value: String,
) {
if (status.value == Status.CONNECTED) {
scope.launch {
val message = Message(from = player.value, type = type, value = value)
val json = Json.encodeToJsonElement(message)
messageResponseFlow.emit(json.toString())
}
}
}
enum class Status {
CONNECTED,
DISCONNECTED
}
enum class Type {
CLIENT,
SERVER,
NONE,
}
}

View file

@ -0,0 +1,32 @@
package com.pixelized.desktop.lwa.repository.network.helper
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.webSocket
import io.ktor.http.HttpMethod
// https://ktor.io/docs/client-websockets.html#handle-session
fun client(): HttpClient {
val client = HttpClient(CIO) {
install(WebSockets) {
pingIntervalMillis = 20_000
}
}
return client
}
suspend fun HttpClient.connectWebSocket(
host: String,
port: Int,
block: suspend DefaultClientWebSocketSession.() -> Unit
) {
webSocket(
method = HttpMethod.Get,
host = host,
port = port,
path = "/ws",
block = block,
)
}

View file

@ -0,0 +1,39 @@
package com.pixelized.desktop.lwa.repository.network.helper
import io.ktor.server.application.install
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.server.routing.routing
import io.ktor.server.websocket.DefaultWebSocketServerSession
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
import io.ktor.server.websocket.timeout
import io.ktor.server.websocket.webSocket
import kotlin.time.Duration.Companion.seconds
// https://ktor.io/docs/server-websockets.html#handle-multiple-session
fun server(
port: Int = 8080,
handler: suspend DefaultWebSocketServerSession.() -> Unit
): EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
return embeddedServer(
factory = Netty,
port = port,
module = {
install(WebSockets) {
pingPeriod = 15.seconds
timeout = 15.seconds
maxFrameSize = Long.MAX_VALUE
masking = false
}
routing {
webSocket(
path = "/ws",
handler = handler,
)
}
},
)
}

View file

@ -0,0 +1,10 @@
package com.pixelized.desktop.lwa.repository.network.protocol
import kotlinx.serialization.Serializable
@Serializable
class Message(
val from: String,
val type: String,
val value: String,
)

View file

@ -0,0 +1,35 @@
package com.pixelized.desktop.lwa.screen.characterSheet
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.navigation.LocalScreenController
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetEditPage
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetPage
@Composable
fun CharacterSheetMainNavHost(
controller: NavHostController = rememberNavController(),
startDestination: String,
) {
CompositionLocalProvider(
LocalScreenController provides controller,
) {
Surface(
modifier = Modifier.fillMaxSize(),
) {
NavHost(
navController = controller,
startDestination = startDestination,
) {
composableCharacterSheetPage()
composableCharacterSheetEditPage()
}
}
}
}

View file

@ -4,13 +4,12 @@ import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@ -36,14 +35,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.composable.overlay.BlurOverlay import com.pixelized.desktop.lwa.composable.overlay.BlurOverlay
import com.pixelized.desktop.lwa.composable.overlay.BlurOverlayViewModel import com.pixelized.desktop.lwa.composable.overlay.BlurOverlayViewModel
import com.pixelized.desktop.lwa.navigation.LocalScreen import com.pixelized.desktop.lwa.navigation.LocalScreenController
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.screen.roll.RollPage import com.pixelized.desktop.lwa.screen.roll.RollPage
import com.pixelized.desktop.lwa.screen.roll.RollViewModel import com.pixelized.desktop.lwa.screen.roll.RollViewModel
@ -95,7 +94,8 @@ fun CharacterSheetPage(
overlayViewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() }, overlayViewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() },
rollViewModel: RollViewModel = viewModel { RollViewModel() }, rollViewModel: RollViewModel = viewModel { RollViewModel() },
) { ) {
val screen = LocalScreen.current val window = LocalWindowController.current
val screen = LocalScreenController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Surface( Surface(
@ -123,7 +123,9 @@ fun CharacterSheetPage(
onDelete = { onDelete = {
scope.launch { scope.launch {
viewModel.deleteCharacter(id = sheet.id) viewModel.deleteCharacter(id = sheet.id)
screen.popBackStack() if (screen.popBackStack().not()) {
window.close()
}
} }
}, },
onCharacteristic = { characteristic -> onCharacteristic = { characteristic ->
@ -148,12 +150,10 @@ fun CharacterSheetPage(
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun CharacterSheetPageContent( fun CharacterSheetPageContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(), scrollState: ScrollState = rememberScrollState(),
width: Dp = 320.dp,
characterSheet: CharacterSheetPageUio, characterSheet: CharacterSheetPageUio,
onBack: () -> Unit, onBack: () -> Unit,
onEdit: () -> Unit, onEdit: () -> Unit,
@ -163,6 +163,7 @@ fun CharacterSheetPageContent(
onRoll: (roll: CharacterSheetPageUio.Roll) -> Unit, onRoll: (roll: CharacterSheetPageUio.Roll) -> Unit,
) { ) {
Scaffold( Scaffold(
modifier = modifier,
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
@ -203,115 +204,131 @@ fun CharacterSheetPageContent(
) )
}, },
content = { paddingValues -> content = { paddingValues ->
Column( Row(
modifier = Modifier modifier = Modifier
.verticalScroll(state = scrollState).padding(all = 16.dp) .verticalScroll(state = scrollState)
.padding(paddingValues) .padding(paddingValues)
.then(other = modifier), .padding(horizontal = 24.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp), horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
FlowRow( Column(
maxItemsInEachRow = 3, modifier = Modifier
horizontalArrangement = Arrangement.spacedBy( .fillMaxHeight()
space = 16.dp, .width(100.dp),
alignment = Alignment.CenterHorizontally,
),
verticalArrangement = Arrangement.spacedBy(space = 16.dp), verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) { ) {
characterSheet.characteristics.forEach { characterSheet.characteristics.forEach {
Stat( Stat(
modifier = Modifier.width(width = width / 3 - 32.dp) modifier = Modifier
.height(height = 112.dp), .fillMaxWidth()
.heightIn(min = 120.dp),
characteristic = it, characteristic = it,
onClick = { onCharacteristic(it) }, onClick = { onCharacteristic(it) },
) )
} }
} }
DecoratedBox( Column(
modifier = Modifier.width(width = width).padding(vertical = 8.dp), modifier = Modifier
.fillMaxHeight()
.weight(2f / 3f),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Column { DecoratedBox(
Text( modifier = Modifier
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), .fillMaxWidth()
style = MaterialTheme.typography.caption, .padding(vertical = 8.dp),
textAlign = TextAlign.Center, ) {
text = stringResource(Res.string.character_sheet__sub_characteristics__title), Column {
) Text(
characterSheet.subCharacteristics.forEach { modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
Characteristics( style = MaterialTheme.typography.caption,
modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center,
characteristic = it, text = stringResource(Res.string.character_sheet__sub_characteristics__title),
) )
characterSheet.subCharacteristics.forEach {
Characteristics(
modifier = Modifier.fillMaxWidth(),
characteristic = it,
)
}
} }
} }
} DecoratedBox(
DecoratedBox( modifier = Modifier
modifier = Modifier.width(width = width).padding(vertical = 8.dp), .fillMaxWidth()
) { .padding(vertical = 8.dp),
Column { ) {
Text( Column {
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), Text(
style = MaterialTheme.typography.caption, modifier = Modifier
textAlign = TextAlign.Center, .fillMaxWidth()
text = stringResource(Res.string.character_sheet__skills__title), .padding(bottom = 8.dp),
) style = MaterialTheme.typography.caption,
characterSheet.skills.forEach { textAlign = TextAlign.Center,
Skill( text = stringResource(Res.string.character_sheet__skills__title),
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
) )
characterSheet.skills.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
} }
} }
} DecoratedBox(
DecoratedBox( modifier = Modifier
modifier = Modifier.width(width = width).padding(vertical = 8.dp), .fillMaxWidth()
) { .padding(vertical = 8.dp),
Column { ) {
Text( Column {
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), Text(
style = MaterialTheme.typography.caption, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
textAlign = TextAlign.Center, style = MaterialTheme.typography.caption,
text = stringResource(Res.string.character_sheet__occupations_title), textAlign = TextAlign.Center,
) text = stringResource(Res.string.character_sheet__occupations_title),
characterSheet.occupations.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
) )
characterSheet.occupations.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
} }
} }
} DecoratedBox(
DecoratedBox( modifier = Modifier
modifier = Modifier.width(width = width).padding(vertical = 8.dp), .fillMaxWidth()
) { .padding(vertical = 8.dp),
Column { ) {
Text( Column {
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp), Text(
style = MaterialTheme.typography.caption, modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
textAlign = TextAlign.Center, style = MaterialTheme.typography.caption,
text = stringResource(Res.string.character_sheet__magics__title), textAlign = TextAlign.Center,
) text = stringResource(Res.string.character_sheet__magics__title),
characterSheet.magics.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
) )
characterSheet.magics.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
} }
} }
} characterSheet.rolls.forEach {
characterSheet.rolls.forEach { Roll(
Roll( modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(), label = it.label,
label = it.label, onClick = { onRoll(it) },
onClick = { onRoll(it) }, )
) }
} }
} }
} }
@ -326,7 +343,9 @@ private fun Stat(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
DecoratedBox( DecoratedBox(
modifier = Modifier.clickable(onClick = onClick).padding(paddingValues = paddingValues) modifier = Modifier
.clickable(onClick = onClick)
.padding(paddingValues = paddingValues)
.then(other = modifier), .then(other = modifier),
) { ) {
Text( Text(

View file

@ -29,8 +29,9 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreen import com.pixelized.desktop.lwa.navigation.LocalScreenController
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -75,7 +76,8 @@ fun CharacterSheetEditPage(
) )
}, },
) { ) {
val screen = LocalScreen.current val window = LocalWindowController.current
val screen = LocalScreenController.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
Surface( Surface(
@ -89,7 +91,9 @@ fun CharacterSheetEditPage(
onSave = { onSave = {
scope.launch { scope.launch {
viewModel.save() viewModel.save()
screen.popBackStack() if (screen.popBackStack().not()) {
window.close()
}
} }
}, },
) )

View file

@ -3,26 +3,40 @@ package com.pixelized.desktop.lwa.screen.main
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.rememberWindowState
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.navigation.LocalScreen import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheet import com.pixelized.desktop.lwa.WindowController
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreenController
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.navigation.destination.navigateToNetwork
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetMainNavHost
import lwacharactersheet.composeapp.generated.resources.Res import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__title
import lwacharactersheet.composeapp.generated.resources.main_page__create_action import lwacharactersheet.composeapp.generated.resources.main_page__create_action
import lwacharactersheet.composeapp.generated.resources.main_page__network_action
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@Stable @Stable
@ -35,62 +49,165 @@ data class CharacterUio(
fun MainPage( fun MainPage(
viewModel: MainPageViewModel = viewModel { MainPageViewModel() }, viewModel: MainPageViewModel = viewModel { MainPageViewModel() },
) { ) {
val screen = LocalScreen.current val screen = LocalScreenController.current
Surface { Surface(
modifier = Modifier.fillMaxSize(),
) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.verticalScroll(state = rememberScrollState())
.fillMaxSize()
.padding(horizontal = 24.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
MainPageContent( MainPageContent(
characters = viewModel.characters, characters = viewModel.characters,
onCharacter = { onCharacter = {
screen.navigateToCharacterSheet(id = it.id) viewModel.showCharacterSheet(sheet = it)
}, },
onCreateCharacter = { onCreateCharacter = {
screen.navigateToCharacterSheetEdit() viewModel.showCreateCharacterSheet()
},
onNetwork = {
screen.navigateToNetwork()
}, },
) )
} }
} }
HandleCharacterSheet(
sheets = viewModel.sheet,
onCloseRequest = { viewModel.hideCharacterSheet(sheet = it) }
)
HandleCharacterSheetCreation(
sheets = viewModel.create,
onCloseRequest = { viewModel.hideCreateCharacterSheet(id = it) },
)
} }
@Composable
fun HandleCharacterSheet(
sheets: State<Set<CharacterUio>>,
onCloseRequest: (id: CharacterUio) -> Unit,
) {
sheets.value.forEach { sheet ->
val controller = remember {
WindowController(
onCloseRequest = { onCloseRequest(sheet) }
)
}
CompositionLocalProvider(
LocalWindowController provides controller,
) {
Window(
onCloseRequest = { onCloseRequest(sheet) },
state = rememberWindowState(
width = 400.dp + 64.dp,
height = 900.dp,
),
title = sheet.name,
) {
CharacterSheetMainNavHost(
startDestination = CharacterSheetDestination.navigationRoute(id = sheet.id)
)
}
}
}
}
@Composable
fun HandleCharacterSheetCreation(
sheets: State<Set<Int>>,
onCloseRequest: (id: Int) -> Unit,
) {
sheets.value.forEach { sheet ->
val controller = remember {
WindowController(
onCloseRequest = { onCloseRequest(sheet) }
)
}
CompositionLocalProvider(
LocalWindowController provides controller,
) {
Window(
onCloseRequest = { controller.close() },
state = rememberWindowState(
width = 400.dp + 64.dp,
height = 900.dp,
),
title = stringResource(Res.string.character_sheet_edit__title),
) {
CharacterSheetMainNavHost(
startDestination = CharacterSheetEditDestination.navigationRoute(id = null)
)
}
}
}
}
@Composable @Composable
fun MainPageContent( fun MainPageContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
characters: State<List<CharacterUio>>, characters: State<List<CharacterUio>>,
onCharacter: (CharacterUio) -> Unit, onCharacter: (CharacterUio) -> Unit,
onCreateCharacter: () -> Unit, onCreateCharacter: () -> Unit,
onNetwork: () -> Unit,
) { ) {
Column( Column(
modifier = modifier.padding(horizontal = 24.dp), modifier = modifier,
verticalArrangement = Arrangement.spacedBy(space = 32.dp),
) { ) {
Column { Spacer(
characters.value.forEach { sheet -> modifier = Modifier.weight(weight = 1f)
)
DecoratedBox {
Column(
modifier = Modifier.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(space = 32.dp),
) {
Column {
characters.value.forEach { sheet ->
TextButton(
onClick = { onCharacter(sheet) },
) {
Text(
modifier = Modifier.fillMaxWidth(),
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
maxLines = 1,
text = sheet.name,
)
}
}
}
TextButton( TextButton(
onClick = { onCharacter(sheet) }, onClick = { onCreateCharacter() },
) { ) {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
maxLines = 1, text = stringResource(Res.string.main_page__create_action),
text = sheet.name,
) )
} }
} }
} }
Spacer(
modifier = Modifier.weight(weight = 1f)
)
TextButton( TextButton(
onClick = { onCreateCharacter() }, onClick = { onNetwork() },
) { ) {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
text = stringResource(Res.string.main_page__create_action), text = stringResource(Res.string.main_page__network_action),
) )
} }
} }

View file

@ -3,18 +3,25 @@ package com.pixelized.desktop.lwa.screen.main
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.utils.extention.collectAsState import com.pixelized.desktop.lwa.utils.extention.collectAsState
class MainPageViewModel : ViewModel() { class MainPageViewModel : ViewModel() {
// using a variable to help with later injection. // using a variable to help with later injection.
private val characterSheetRepository = CharacterSheetRepository private val repository = CharacterSheetRepository
private val _sheet = mutableStateOf<Set<CharacterUio>>(emptySet())
val sheet: State<Set<CharacterUio>> get() = _sheet
private val _create = mutableStateOf<Set<Int>>(emptySet())
val create: State<Set<Int>> get() = _create
val characters: State<List<CharacterUio>> val characters: State<List<CharacterUio>>
@Composable @Composable
@Stable @Stable
get() = characterSheetRepository get() = repository
.characterSheetFlow() .characterSheetFlow()
.collectAsState { sheets -> .collectAsState { sheets ->
sheets.map { sheet -> sheets.map { sheet ->
@ -24,4 +31,20 @@ class MainPageViewModel : ViewModel() {
) )
} }
} }
fun showCreateCharacterSheet() {
_create.value = _create.value.toMutableSet().apply { add(size) }
}
fun hideCreateCharacterSheet(id: Int) {
_create.value = _create.value.toMutableSet().apply { remove(id) }
}
fun showCharacterSheet(sheet: CharacterUio) {
_sheet.value = _sheet.value.toMutableSet().apply { add(sheet) }
}
fun hideCharacterSheet(sheet: CharacterUio) {
_sheet.value = _sheet.value.toMutableSet().apply { remove(sheet) }
}
} }

View file

@ -0,0 +1,24 @@
package com.pixelized.desktop.lwa.screen.network
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Type
class NetworkFactory {
fun convertToUio(
player: String,
status: Status,
type: Type,
host: String,
port: Int,
): NetworkPageUio {
return NetworkPageUio(
player = player,
host = host,
port = "$port",
enableFields = status == Status.DISCONNECTED,
enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0,
enableCancel = type != Type.NONE && status == Status.CONNECTED
)
}
}

View file

@ -0,0 +1,176 @@
package com.pixelized.desktop.lwa.screen.network
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TextField
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.navigation.LocalScreenController
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.network__host__label
import lwacharactersheet.composeapp.generated.resources.network__player_name__label
import lwacharactersheet.composeapp.generated.resources.network__port__label
import lwacharactersheet.composeapp.generated.resources.network__socket__disconnect_action
import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action
import lwacharactersheet.composeapp.generated.resources.network__socket__host_action
import lwacharactersheet.composeapp.generated.resources.network__title
import org.jetbrains.compose.resources.stringResource
@Stable
data class NetworkPageUio(
val player: String,
val host: String,
val port: String,
val enableFields: Boolean,
val enableActions: Boolean,
val enableCancel: Boolean,
)
@Composable
fun NetworkPage(
viewModel: NetworkViewModel = viewModel { NetworkViewModel() },
) {
val screen = LocalScreenController.current
Surface(
modifier = Modifier.fillMaxSize(),
) {
NetworkContent(
modifier = Modifier.fillMaxSize(),
player = viewModel.network,
onBack = { screen.popBackStack() },
onPlayerChange = viewModel::onPlayerNameChange,
onHostChange = viewModel::onHostChange,
onPortChange = viewModel::onPortChange,
onConnect = viewModel::connect,
onHost = viewModel::host,
onDisconnect = viewModel::disconnect,
)
}
}
@Composable
private fun NetworkContent(
modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(),
player: State<NetworkPageUio>,
onBack: () -> Unit,
onPlayerChange: (String) -> Unit,
onHostChange: (String) -> Unit,
onPortChange: (String) -> Unit,
onConnect: () -> Unit,
onHost: () -> Unit,
onDisconnect: () -> Unit,
) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
title = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = stringResource(Res.string.network__title),
)
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = { paddingValues ->
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(paddingValues)
.padding(all = 16.dp),
) {
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__player_name__label)) },
onValueChange = { onPlayerChange(it) },
value = player.value.player,
)
Spacer(
modifier = Modifier.height(16.dp),
)
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
TextField(
modifier = Modifier.weight(1f),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__host__label)) },
onValueChange = { onHostChange(it) },
value = player.value.host,
)
TextField(
modifier = Modifier.width(100.dp),
singleLine = true,
enabled = player.value.enableFields,
label = { Text(text = stringResource(Res.string.network__port__label)) },
onValueChange = { onPortChange(it) },
value = player.value.port,
)
}
TextButton(
enabled = player.value.enableActions,
onClick = onConnect,
) {
Text(text = stringResource(Res.string.network__socket__connect_action))
}
TextButton(
enabled = player.value.enableActions,
onClick = onHost,
) {
Text(text = stringResource(Res.string.network__socket__host_action))
}
TextButton(
enabled = player.value.enableCancel,
onClick = onDisconnect,
) {
Text(text = stringResource(Res.string.network__socket__disconnect_action))
}
}
}
)
}

View file

@ -0,0 +1,62 @@
package com.pixelized.desktop.lwa.screen.network
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
class NetworkViewModel : ViewModel() {
private val repository = NetworkRepository
private val factory = NetworkFactory()
private val host = mutableStateOf("localhost")
private val port = mutableStateOf(27030)
val network: State<NetworkPageUio>
@Composable
@Stable
get() {
val player = repository.player.collectAsState()
val status = repository.status.collectAsState()
val type = repository.type.collectAsState()
return remember {
derivedStateOf {
factory.convertToUio(
player = player.value,
status = status.value,
type = type.value,
host = host.value,
port = port.value,
)
}
}
}
fun onPlayerNameChange(player: String) {
repository.onPlayerNameChange(player = player)
}
fun onPortChange(port: String) {
this.port.value = port.toIntOrNull() ?: 0
}
fun onHostChange(host: String) {
this.host.value = host
}
fun host() {
repository.host(port = port.value)
}
fun connect() {
repository.connect(host = host.value, port = port.value)
}
fun disconnect() {
repository.disconnect()
}
}

View file

@ -109,7 +109,7 @@ fun RollPage(
this.alpha = 0.8f this.alpha = 0.8f
this.rotationZ = viewModel.rollRotation.value this.rotationZ = viewModel.rollRotation.value
}, },
tint = MaterialTheme.colors.onSurface, tint = MaterialTheme.colors.primary,
painter = painterResource(Res.drawable.ic_d20_32dp), painter = painterResource(Res.drawable.ic_d20_32dp),
contentDescription = null, contentDescription = null,
) )

View file

@ -10,6 +10,7 @@ import com.pixelized.desktop.lwa.business.RollUseCase
import com.pixelized.desktop.lwa.business.SkillStepUseCase import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -25,6 +26,8 @@ import lwacharactersheet.composeapp.generated.resources.roll_page__success
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
class RollViewModel : ViewModel() { class RollViewModel : ViewModel() {
private val network = NetworkRepository
private val _roll = mutableStateOf(RollUio(label = "", value = 0)) private val _roll = mutableStateOf(RollUio(label = "", value = 0))
val roll: State<RollUio> get() = _roll val roll: State<RollUio> get() = _roll
@ -134,8 +137,14 @@ class RollViewModel : ViewModel() {
} ?: "", } ?: "",
value = roll, value = roll,
) )
share(roll = roll)
} }
} }
} }
} }
private fun share(roll: Int) {
network.share(type = "roll", value = "$roll")
}
} }

View file

@ -1,21 +1,9 @@
package com.pixelized.desktop.lwa package com.pixelized.desktop.lwa
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
fun main() { fun main() {
application { application {
Window( App()
onCloseRequest = ::exitApplication,
state = rememberWindowState(
width = 320.dp + 64.dp,
height = 900.dp,
),
title = "LwaCharacterSheet",
) {
App()
}
} }
} }

View file

@ -6,22 +6,27 @@ junit = "4.13.2"
compose-multiplatform = "1.7.0" compose-multiplatform = "1.7.0"
androidx-lifecycle = "2.8.3" androidx-lifecycle = "2.8.3"
androidx-navigation = "2.8.0-alpha10" androidx-navigation = "2.8.0-alpha10"
ktor_version = "3.0.1"
[libraries]
# Test
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
# Compose
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-json" }
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinKtor = { id = "io.ktor.plugin", version.ref = "ktor_version"}
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
androidx-navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-json" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor_version" }
ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor_version" }
ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor_version" }
ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref = "ktor_version" }
ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor_version" }
ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor_version" }