Add server & shared module and remove the serveur from the client app.
|
|
@ -1,9 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
// this is necessary to avoid the plugins to be loaded multiple times
|
|
||||||
// in each subproject's classloader
|
|
||||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
|
||||||
alias(libs.plugins.kotlinSerialization) apply false
|
|
||||||
// alias(libs.plugins.kotlinKtor) apply false
|
|
||||||
alias(libs.plugins.composeMultiplatform) apply false
|
alias(libs.plugins.composeMultiplatform) apply false
|
||||||
|
alias(libs.plugins.kotlinSerialization) apply false
|
||||||
alias(libs.plugins.composeCompiler) apply false
|
alias(libs.plugins.composeCompiler) apply false
|
||||||
|
alias(libs.plugins.kotlinJvm) apply false
|
||||||
|
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||||
}
|
}
|
||||||
|
|
@ -29,16 +29,15 @@ kotlin {
|
||||||
api(libs.koin.core)
|
api(libs.koin.core)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
implementation(libs.koin.compose.viewmodel)
|
implementation(libs.koin.compose.viewmodel)
|
||||||
// network.
|
// common
|
||||||
|
implementation(projects.shared)
|
||||||
|
// network
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.cio)
|
||||||
implementation(libs.ktor.client.websockets)
|
implementation(libs.ktor.client.websockets)
|
||||||
implementation(libs.ktor.server.core)
|
|
||||||
implementation(libs.ktor.server.netty)
|
|
||||||
implementation(libs.ktor.server.websockets)
|
|
||||||
// shell
|
// shell
|
||||||
implementation(libs.lordcodes.turttle)
|
implementation(libs.turtle)
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,6 @@
|
||||||
<string name="network__host__label">host</string>
|
<string name="network__host__label">host</string>
|
||||||
<string name="network__port__label">port</string>
|
<string name="network__port__label">port</string>
|
||||||
<string name="network__socket__connect_action">Se connecter à la table</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__disconnect_action">Se déconnecter</string>
|
||||||
<string name="network__socket__status_state">État de connexion : %1$s</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_connected">Connecté</string>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
package com.pixelized.desktop.lwa.repository.network
|
package com.pixelized.desktop.lwa.repository.network
|
||||||
|
|
||||||
|
//import com.pixelized.desktop.lwa.repository.network.helper.server
|
||||||
|
//import io.ktor.server.engine.EmbeddedServer
|
||||||
|
//import io.ktor.server.netty.NettyApplicationEngine
|
||||||
import com.pixelized.desktop.lwa.repository.network.helper.client
|
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.connectWebSocket
|
||||||
import com.pixelized.desktop.lwa.repository.network.helper.server
|
import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame
|
||||||
import com.pixelized.desktop.lwa.repository.network.protocol.Message
|
import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
|
||||||
import com.pixelized.desktop.lwa.repository.network.protocol.MessageContent
|
import com.pixelized.server.lwa.SERVER_PORT
|
||||||
|
import com.pixelized.server.lwa.protocol.Message
|
||||||
|
import com.pixelized.server.lwa.protocol.MessageContent
|
||||||
import io.ktor.client.HttpClient
|
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.Frame
|
||||||
import io.ktor.websocket.readText
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
|
@ -21,20 +23,17 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
|
||||||
|
|
||||||
typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>
|
|
||||||
typealias Client = HttpClient
|
typealias Client = HttpClient
|
||||||
|
|
||||||
class NetworkRepository {
|
class NetworkRepository {
|
||||||
companion object {
|
companion object {
|
||||||
const val DEFAULT_PORT = 16030
|
const val DEFAULT_PORT = SERVER_PORT
|
||||||
const val DEFAULT_HOST = "pixelized.freeboxos.fr"
|
const val DEFAULT_HOST = "pixelized.freeboxos.fr"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
private var networkJob: Job? = null
|
private var networkJob: Job? = null
|
||||||
private var server: Server? = null
|
|
||||||
private var client: Client? = null
|
private var client: Client? = null
|
||||||
|
|
||||||
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
|
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
|
||||||
|
|
@ -47,65 +46,14 @@ class NetworkRepository {
|
||||||
private val _status = MutableStateFlow(Status.DISCONNECTED)
|
private val _status = MutableStateFlow(Status.DISCONNECTED)
|
||||||
val status: StateFlow<Status> get() = _status
|
val status: StateFlow<Status> get() = _status
|
||||||
|
|
||||||
private val _type = MutableStateFlow(Type.NONE)
|
|
||||||
val type: StateFlow<Type> get() = _type
|
|
||||||
|
|
||||||
fun onPlayerNameChange(player: String) {
|
fun onPlayerNameChange(player: String) {
|
||||||
_player.value = player
|
_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 {
|
|
||||||
// send local message to the clients
|
|
||||||
outgoingMessageBuffer.collect { message ->
|
|
||||||
send(Json.encodeToFrame(message = message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
runCatching {
|
|
||||||
// watching for clients incoming message
|
|
||||||
incoming.consumeEach { frame ->
|
|
||||||
if (frame is Frame.Text) {
|
|
||||||
val message = Json.decodeFromFrame(frame = frame)
|
|
||||||
incomingMessageBuffer.emit(message)
|
|
||||||
// broadcast to clients the message
|
|
||||||
outgoingMessageBuffer.emit(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.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(
|
fun connect(
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
onConnect: (Type) -> Unit = { },
|
onConnect: () -> Unit = { },
|
||||||
onFailure: (Exception) -> Unit = { },
|
onFailure: (Exception) -> Unit = { },
|
||||||
onClose: () -> Unit = { },
|
onClose: () -> Unit = { },
|
||||||
) {
|
) {
|
||||||
|
|
@ -115,9 +63,8 @@ class NetworkRepository {
|
||||||
networkJob = scope.launch {
|
networkJob = scope.launch {
|
||||||
try {
|
try {
|
||||||
client?.connectWebSocket(host = host, port = port) {
|
client?.connectWebSocket(host = host, port = port) {
|
||||||
_type.value = Type.CLIENT
|
|
||||||
_status.value = Status.CONNECTED
|
_status.value = Status.CONNECTED
|
||||||
onConnect(Type.CLIENT)
|
onConnect()
|
||||||
|
|
||||||
val job = launch {
|
val job = launch {
|
||||||
// send message to the server
|
// send message to the server
|
||||||
|
|
@ -140,7 +87,6 @@ class NetworkRepository {
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
onFailure(exception)
|
onFailure(exception)
|
||||||
} finally {
|
} finally {
|
||||||
_type.value = Type.NONE
|
|
||||||
_status.value = Status.DISCONNECTED
|
_status.value = Status.DISCONNECTED
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +96,6 @@ class NetworkRepository {
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
networkJob?.cancel()
|
networkJob?.cancel()
|
||||||
scope.launch {
|
scope.launch {
|
||||||
server?.stop()
|
|
||||||
client?.close()
|
client?.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,10 +110,6 @@ class NetworkRepository {
|
||||||
)
|
)
|
||||||
// emit the message into the outgoing buffer
|
// emit the message into the outgoing buffer
|
||||||
outgoingMessageBuffer.emit(message)
|
outgoingMessageBuffer.emit(message)
|
||||||
// emit the message into the incoming buffer IF we are the server
|
|
||||||
if (type.value == Type.SERVER) {
|
|
||||||
incomingMessageBuffer.emit(message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,20 +117,4 @@ class NetworkRepository {
|
||||||
CONNECTED,
|
CONNECTED,
|
||||||
DISCONNECTED
|
DISCONNECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Type {
|
|
||||||
CLIENT,
|
|
||||||
SERVER,
|
|
||||||
NONE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Json.decodeFromFrame(frame: Frame.Text): Message {
|
|
||||||
val json = frame.readText()
|
|
||||||
return decodeFromString<Message>(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Json.encodeToFrame(message: Message): Frame {
|
|
||||||
val json = encodeToJsonElement(message)
|
|
||||||
return Frame.Text(text = json.toString())
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
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,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package com.pixelized.desktop.lwa.repository.roll_history
|
package com.pixelized.desktop.lwa.repository.roll_history
|
||||||
|
|
||||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
|
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
|
||||||
import com.pixelized.desktop.lwa.repository.network.protocol.Message
|
import com.pixelized.server.lwa.protocol.Message
|
||||||
import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage
|
import com.pixelized.server.lwa.protocol.RollMessage
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
package com.pixelized.desktop.lwa.screen.network
|
package com.pixelized.desktop.lwa.screen.network
|
||||||
|
|
||||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
|
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
|
||||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Type
|
|
||||||
|
|
||||||
class NetworkFactory {
|
class NetworkFactory {
|
||||||
|
|
||||||
fun convertToUio(
|
fun convertToUio(
|
||||||
player: String,
|
player: String,
|
||||||
status: Status,
|
status: Status,
|
||||||
type: Type,
|
|
||||||
host: String,
|
host: String,
|
||||||
port: Int,
|
port: Int,
|
||||||
): NetworkPageUio {
|
): NetworkPageUio {
|
||||||
|
|
@ -18,7 +16,7 @@ class NetworkFactory {
|
||||||
port = "$port",
|
port = "$port",
|
||||||
enableFields = status == Status.DISCONNECTED,
|
enableFields = status == Status.DISCONNECTED,
|
||||||
enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0,
|
enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0,
|
||||||
enableCancel = type != Type.NONE && status == Status.CONNECTED
|
enableCancel = status == Status.CONNECTED
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,11 +48,9 @@ import lwacharactersheet.composeapp.generated.resources.network__player_name__la
|
||||||
import lwacharactersheet.composeapp.generated.resources.network__port__label
|
import lwacharactersheet.composeapp.generated.resources.network__port__label
|
||||||
import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action
|
import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action
|
||||||
import lwacharactersheet.composeapp.generated.resources.network__socket__disconnect_action
|
import lwacharactersheet.composeapp.generated.resources.network__socket__disconnect_action
|
||||||
import lwacharactersheet.composeapp.generated.resources.network__socket__host_action
|
|
||||||
import lwacharactersheet.composeapp.generated.resources.network__title
|
import lwacharactersheet.composeapp.generated.resources.network__title
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import org.koin.core.annotation.KoinExperimentalAPI
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class NetworkPageUio(
|
data class NetworkPageUio(
|
||||||
|
|
@ -64,7 +62,6 @@ data class NetworkPageUio(
|
||||||
val enableCancel: Boolean,
|
val enableCancel: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(KoinExperimentalAPI::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NetworkPage(
|
fun NetworkPage(
|
||||||
viewModel: NetworkViewModel = koinViewModel(),
|
viewModel: NetworkViewModel = koinViewModel(),
|
||||||
|
|
@ -91,7 +88,6 @@ fun NetworkPage(
|
||||||
onHostChange = viewModel::onHostChange,
|
onHostChange = viewModel::onHostChange,
|
||||||
onPortChange = viewModel::onPortChange,
|
onPortChange = viewModel::onPortChange,
|
||||||
onConnect = viewModel::connect,
|
onConnect = viewModel::connect,
|
||||||
onHost = viewModel::host,
|
|
||||||
onDisconnect = viewModel::disconnect,
|
onDisconnect = viewModel::disconnect,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +129,6 @@ private fun NetworkContent(
|
||||||
onHostChange: (String) -> Unit,
|
onHostChange: (String) -> Unit,
|
||||||
onPortChange: (String) -> Unit,
|
onPortChange: (String) -> Unit,
|
||||||
onConnect: () -> Unit,
|
onConnect: () -> Unit,
|
||||||
onHost: () -> Unit,
|
|
||||||
onDisconnect: () -> Unit,
|
onDisconnect: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -207,13 +202,6 @@ private fun NetworkContent(
|
||||||
Text(text = stringResource(Res.string.network__socket__connect_action))
|
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(
|
TextButton(
|
||||||
enabled = player.value.enableCancel,
|
enabled = player.value.enableCancel,
|
||||||
onClick = onDisconnect,
|
onClick = onDisconnect,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ class NetworkViewModel(
|
||||||
private val repository: NetworkRepository,
|
private val repository: NetworkRepository,
|
||||||
private val factory: NetworkFactory
|
private val factory: NetworkFactory
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST)
|
private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST)
|
||||||
private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT)
|
private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT)
|
||||||
|
|
||||||
|
|
@ -41,13 +40,11 @@ class NetworkViewModel(
|
||||||
get() {
|
get() {
|
||||||
val player = repository.player.collectAsState()
|
val player = repository.player.collectAsState()
|
||||||
val status = repository.status.collectAsState()
|
val status = repository.status.collectAsState()
|
||||||
val type = repository.type.collectAsState()
|
|
||||||
return remember {
|
return remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
factory.convertToUio(
|
factory.convertToUio(
|
||||||
player = player.value,
|
player = player.value,
|
||||||
status = status.value,
|
status = status.value,
|
||||||
type = type.value,
|
|
||||||
host = host.value,
|
host = host.value,
|
||||||
port = port.value,
|
port = port.value,
|
||||||
)
|
)
|
||||||
|
|
@ -67,10 +64,6 @@ class NetworkViewModel(
|
||||||
this.host.value = host
|
this.host.value = host
|
||||||
}
|
}
|
||||||
|
|
||||||
fun host() {
|
|
||||||
repository.host(port = port.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun connect() {
|
fun connect() {
|
||||||
controller.show()
|
controller.show()
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage
|
import com.pixelized.server.lwa.protocol.RollMessage
|
||||||
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
|
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.pixelized.desktop.lwa.utils.extention
|
||||||
|
|
||||||
|
import com.pixelized.server.lwa.protocol.Message
|
||||||
|
import io.ktor.websocket.Frame
|
||||||
|
import io.ktor.websocket.readText
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
|
||||||
|
fun Json.decodeFromFrame(frame: Frame.Text): Message {
|
||||||
|
val json = frame.readText()
|
||||||
|
return decodeFromString<Message>(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Json.encodeToFrame(message: Message): Frame {
|
||||||
|
val json = encodeToJsonElement(message)
|
||||||
|
return Frame.Text(text = json.toString())
|
||||||
|
}
|
||||||
|
|
@ -3,4 +3,7 @@ kotlin.code.style=official
|
||||||
kotlin.daemon.jvmargs=-Xmx2048M
|
kotlin.daemon.jvmargs=-Xmx2048M
|
||||||
|
|
||||||
#Gradle
|
#Gradle
|
||||||
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
|
||||||
|
|
||||||
|
#Ktor
|
||||||
|
io.ktor.development=true
|
||||||
|
|
@ -5,14 +5,19 @@ kotlinx-json = "1.7.3"
|
||||||
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.0"
|
ktor = "3.0.1"
|
||||||
koin = "4.0.0"
|
koin = "4.0.0"
|
||||||
|
turtle = "0.5.0"
|
||||||
|
logback = "1.5.11"
|
||||||
|
|
||||||
|
|
||||||
[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" }
|
||||||
|
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", 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" }
|
||||||
|
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||||
|
|
@ -29,11 +34,12 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||||
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
|
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
|
||||||
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
|
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
|
||||||
|
|
||||||
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor_version" }
|
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_version" }
|
ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor" }
|
||||||
ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor_version" }
|
ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor" }
|
||||||
ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref = "ktor_version" }
|
ktor-server-core = { group = 'io.ktor', name = "ktor-server-core", version.ref = "ktor" }
|
||||||
ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor_version" }
|
ktor-server-netty = { group = 'io.ktor', name = "ktor-server-netty", version.ref = "ktor" }
|
||||||
ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor_version" }
|
ktor-server-websockets = { group = 'io.ktor', name = "ktor-server-websockets", version.ref = "ktor" }
|
||||||
|
|
||||||
lordcodes-turttle = { group="com.lordcodes.turtle", name="turtle", version="0.5.0"}
|
turtle = { group = "com.lordcodes.turtle", name = "turtle", version.ref = "turtle" }
|
||||||
|
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
1
server/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
/build
|
|
||||||
|
|
@ -1,44 +1,23 @@
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.androidApplication)
|
alias(libs.plugins.kotlinJvm)
|
||||||
alias(libs.plugins.kotlinAndroid)
|
alias(libs.plugins.ktor)
|
||||||
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
group = "com.pixelized.server.lwa"
|
||||||
namespace = "com.pixelized.server"
|
version = "1.0.0"
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
application {
|
||||||
applicationId = "com.pixelized.server"
|
mainClass.set("com.pixelized.server.lwa.ApplicationKt")
|
||||||
minSdk = 24
|
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}")
|
||||||
targetSdk = 34
|
|
||||||
versionCode = 1
|
|
||||||
versionName = "1.0"
|
|
||||||
|
|
||||||
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
isMinifyEnabled = false
|
|
||||||
proguardFiles(
|
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(projects.shared)
|
||||||
implementation(libs.appcompat.v7)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
testImplementation(libs.junit)
|
implementation(libs.logback)
|
||||||
androidTestImplementation(libs.runner)
|
implementation(libs.ktor.server.core)
|
||||||
androidTestImplementation(libs.espresso.core)
|
implementation(libs.ktor.server.netty)
|
||||||
|
implementation(libs.ktor.server.websockets)
|
||||||
}
|
}
|
||||||
21
server/proguard-rules.pro
vendored
|
|
@ -1,21 +0,0 @@
|
||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
package com.pixelized.server
|
|
||||||
|
|
||||||
import android.support.test.InstrumentationRegistry
|
|
||||||
import android.support.test.runner.AndroidJUnit4
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("com.pixelized.server", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.LwaCharacterSheet" />
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.pixelized.server.lwa
|
||||||
|
|
||||||
|
import com.pixelized.server.lwa.server.LocalServer
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
LocalServer().create().start()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.pixelized.server.lwa.extention
|
||||||
|
|
||||||
|
import com.pixelized.server.lwa.protocol.Message
|
||||||
|
import io.ktor.websocket.Frame
|
||||||
|
import io.ktor.websocket.readText
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
|
||||||
|
fun Json.decodeFromFrame(frame: Frame.Text): Message {
|
||||||
|
val json = frame.readText()
|
||||||
|
return decodeFromString<Message>(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Json.encodeToFrame(message: Message): Frame {
|
||||||
|
val json = encodeToJsonElement(message)
|
||||||
|
return Frame.Text(text = json.toString())
|
||||||
|
}
|
||||||
101
server/src/main/kotlin/com/pixelized/server/lwa/server/Server.kt
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
package com.pixelized.server.lwa.server
|
||||||
|
|
||||||
|
import com.pixelized.server.lwa.SERVER_PORT
|
||||||
|
import com.pixelized.server.lwa.extention.decodeFromFrame
|
||||||
|
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 io.ktor.websocket.Frame
|
||||||
|
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.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
// https://ktor.io/docs/server-websockets.html#handle-multiple-session
|
||||||
|
typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>
|
||||||
|
|
||||||
|
class LocalServer {
|
||||||
|
private var server: Server? = null
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
private var networkJob: Job? = null
|
||||||
|
private val outgoingMessageBuffer = MutableSharedFlow<Frame>()
|
||||||
|
|
||||||
|
fun create(): LocalServer {
|
||||||
|
server = build {
|
||||||
|
println("Server launched")
|
||||||
|
|
||||||
|
val job = launch {
|
||||||
|
// send local message to the clients
|
||||||
|
outgoingMessageBuffer.collect { frame ->
|
||||||
|
send(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
// watching for clients incoming message
|
||||||
|
incoming.consumeEach { frame ->
|
||||||
|
if (frame is Frame.Text) {
|
||||||
|
val message = Json.decodeFromFrame(frame = frame)
|
||||||
|
println(message)
|
||||||
|
// broadcast to clients the message
|
||||||
|
outgoingMessageBuffer.emit(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure { exception ->
|
||||||
|
println("WebSocket exception: ${exception.localizedMessage}")
|
||||||
|
}.also {
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
runBlocking {
|
||||||
|
try {
|
||||||
|
server?.start(wait = true)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
// TODO
|
||||||
|
println("WebSocket exception: ${exception.localizedMessage}")
|
||||||
|
} finally {
|
||||||
|
println("Server close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun build(
|
||||||
|
port: Int = SERVER_PORT,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
|
@ -1,10 +0,0 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.LwaCharacterSheet" parent="Theme.AppCompat.Light.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
|
||||||
<item name="colorPrimaryDark">@color/purple_700</item>
|
|
||||||
<item name="colorAccent">@color/teal_200</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
<resources>
|
|
||||||
<string name="app_name">server</string>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.LwaCharacterSheet" parent="Theme.AppCompat.Light.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_500</item>
|
|
||||||
<item name="colorPrimaryDark">@color/purple_700</item>
|
|
||||||
<item name="colorAccent">@color/teal_200</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
12
server/src/main/resources/logback.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<configuration>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<root level="trace">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||||
|
<logger name="io.netty" level="INFO"/>
|
||||||
|
</configuration>
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package com.pixelized.server
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
fun addition_isCorrect() {
|
|
||||||
assertEquals(4, 2 + 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -29,3 +29,5 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":composeApp")
|
include(":composeApp")
|
||||||
|
include(":server")
|
||||||
|
include(":shared")
|
||||||
15
shared/build.gradle.kts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.kotlinSerialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain.dependencies {
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.pixelized.server.lwa
|
||||||
|
|
||||||
|
const val SERVER_PORT = 16030
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelized.desktop.lwa.repository.network.protocol
|
package com.pixelized.server.lwa.protocol
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelized.desktop.lwa.repository.network.protocol
|
package com.pixelized.server.lwa.protocol
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelized.desktop.lwa.repository.network.protocol
|
package com.pixelized.server.lwa.protocol
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||