Add server & shared module and remove the serveur from the client app.
|
|
@ -1,9 +1,7 @@
|
|||
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.kotlinSerialization) 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)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
// network.
|
||||
// common
|
||||
implementation(projects.shared)
|
||||
// network
|
||||
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)
|
||||
// shell
|
||||
implementation(libs.lordcodes.turttle)
|
||||
implementation(libs.turtle)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
|
|
|
|||
|
|
@ -136,7 +136,6 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
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.connectWebSocket
|
||||
import com.pixelized.desktop.lwa.repository.network.helper.server
|
||||
import com.pixelized.desktop.lwa.repository.network.protocol.Message
|
||||
import com.pixelized.desktop.lwa.repository.network.protocol.MessageContent
|
||||
import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame
|
||||
import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
|
||||
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.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
|
||||
|
|
@ -21,20 +23,17 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
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
|
||||
|
||||
class NetworkRepository {
|
||||
companion object {
|
||||
const val DEFAULT_PORT = 16030
|
||||
const val DEFAULT_PORT = SERVER_PORT
|
||||
const val DEFAULT_HOST = "pixelized.freeboxos.fr"
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private var networkJob: Job? = null
|
||||
private var server: Server? = null
|
||||
private var client: Client? = null
|
||||
|
||||
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
|
||||
|
|
@ -47,65 +46,14 @@ class NetworkRepository {
|
|||
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 {
|
||||
// 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(
|
||||
host: String,
|
||||
port: Int,
|
||||
onConnect: (Type) -> Unit = { },
|
||||
onConnect: () -> Unit = { },
|
||||
onFailure: (Exception) -> Unit = { },
|
||||
onClose: () -> Unit = { },
|
||||
) {
|
||||
|
|
@ -115,9 +63,8 @@ class NetworkRepository {
|
|||
networkJob = scope.launch {
|
||||
try {
|
||||
client?.connectWebSocket(host = host, port = port) {
|
||||
_type.value = Type.CLIENT
|
||||
_status.value = Status.CONNECTED
|
||||
onConnect(Type.CLIENT)
|
||||
onConnect()
|
||||
|
||||
val job = launch {
|
||||
// send message to the server
|
||||
|
|
@ -140,7 +87,6 @@ class NetworkRepository {
|
|||
} catch (exception: Exception) {
|
||||
onFailure(exception)
|
||||
} finally {
|
||||
_type.value = Type.NONE
|
||||
_status.value = Status.DISCONNECTED
|
||||
onClose()
|
||||
}
|
||||
|
|
@ -150,7 +96,6 @@ class NetworkRepository {
|
|||
fun disconnect() {
|
||||
networkJob?.cancel()
|
||||
scope.launch {
|
||||
server?.stop()
|
||||
client?.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -165,10 +110,6 @@ class NetworkRepository {
|
|||
)
|
||||
// emit the message into the outgoing buffer
|
||||
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,
|
||||
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
|
||||
|
||||
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
|
||||
import com.pixelized.desktop.lwa.repository.network.protocol.Message
|
||||
import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage
|
||||
import com.pixelized.server.lwa.protocol.Message
|
||||
import com.pixelized.server.lwa.protocol.RollMessage
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
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 {
|
||||
|
|
@ -18,7 +16,7 @@ class NetworkFactory {
|
|||
port = "$port",
|
||||
enableFields = status == Status.DISCONNECTED,
|
||||
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__socket__connect_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 org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
|
||||
@Stable
|
||||
data class NetworkPageUio(
|
||||
|
|
@ -64,7 +62,6 @@ data class NetworkPageUio(
|
|||
val enableCancel: Boolean,
|
||||
)
|
||||
|
||||
@OptIn(KoinExperimentalAPI::class)
|
||||
@Composable
|
||||
fun NetworkPage(
|
||||
viewModel: NetworkViewModel = koinViewModel(),
|
||||
|
|
@ -91,7 +88,6 @@ fun NetworkPage(
|
|||
onHostChange = viewModel::onHostChange,
|
||||
onPortChange = viewModel::onPortChange,
|
||||
onConnect = viewModel::connect,
|
||||
onHost = viewModel::host,
|
||||
onDisconnect = viewModel::disconnect,
|
||||
)
|
||||
}
|
||||
|
|
@ -133,7 +129,6 @@ private fun NetworkContent(
|
|||
onHostChange: (String) -> Unit,
|
||||
onPortChange: (String) -> Unit,
|
||||
onConnect: () -> Unit,
|
||||
onHost: () -> Unit,
|
||||
onDisconnect: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -207,13 +202,6 @@ private fun NetworkContent(
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class NetworkViewModel(
|
|||
private val repository: NetworkRepository,
|
||||
private val factory: NetworkFactory
|
||||
) : ViewModel() {
|
||||
|
||||
private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST)
|
||||
private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT)
|
||||
|
||||
|
|
@ -41,13 +40,11 @@ class NetworkViewModel(
|
|||
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,
|
||||
)
|
||||
|
|
@ -67,10 +64,6 @@ class NetworkViewModel(
|
|||
this.host.value = host
|
||||
}
|
||||
|
||||
fun host() {
|
||||
repository.host(port = port.value)
|
||||
}
|
||||
|
||||
fun connect() {
|
||||
controller.show()
|
||||
_isLoading.value = true
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage
|
||||
import com.pixelized.server.lwa.protocol.RollMessage
|
||||
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
|
||||
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
|
||||
|
||||
#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"
|
||||
androidx-lifecycle = "2.8.3"
|
||||
androidx-navigation = "2.8.0-alpha10"
|
||||
ktor_version = "3.0.0"
|
||||
ktor = "3.0.1"
|
||||
koin = "4.0.0"
|
||||
turtle = "0.5.0"
|
||||
logback = "1.5.11"
|
||||
|
||||
|
||||
[plugins]
|
||||
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
|
||||
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" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
||||
|
||||
[libraries]
|
||||
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-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-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" }
|
||||
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-cio = { group = 'io.ktor', name = "ktor-client-cio", version.ref = "ktor" }
|
||||
ktor-client-websockets = { group = 'io.ktor', name = "ktor-client-websockets", version.ref = "ktor" }
|
||||
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" }
|
||||
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 {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.kotlinAndroid)
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.ktor)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
application
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.pixelized.server"
|
||||
compileSdk = 34
|
||||
group = "com.pixelized.server.lwa"
|
||||
version = "1.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.pixelized.server"
|
||||
minSdk = 24
|
||||
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"
|
||||
}
|
||||
application {
|
||||
mainClass.set("com.pixelized.server.lwa.ApplicationKt")
|
||||
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.appcompat.v7)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.runner)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
implementation(projects.shared)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.logback)
|
||||
implementation(libs.ktor.server.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)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,4 +28,6 @@ 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
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.desktop.lwa.repository.network.protocol
|
||||
package com.pixelized.server.lwa.protocol
|
||||
|
||||
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
|
||||
|
||||