fix webmap after multi-server refactor, and add styling

This commit is contained in:
Adam 2022-09-02 23:22:18 +02:00
parent 8a6c49a74b
commit 3976c3cc1b
14 changed files with 12461 additions and 96 deletions

View file

@ -1490,7 +1490,7 @@ http-parser-js@>=0.5.1:
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==
"http-proxy-middleware@>=2.0.6 <3.0.0", http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6: http-proxy-middleware@^2.0.3:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f"
integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==

View file

@ -158,3 +158,18 @@ val dockerEnvUpdate by tasks.registering(DockerEnvUpdateTask::class) {
) )
} }
tasks.assemble { dependsOn(dockerEnvUpdate) } tasks.assemble { dependsOn(dockerEnvUpdate) }
val installThemeCss by tasks.registering(Copy::class) {
// val bootswatchThemeCss = resources.text.fromUri("https://bootswatch.com/5/darkly/bootstrap.css")
val bootswatchThemeMinCss =
resources.text.fromUri("https://bootswatch.com/5/darkly/bootstrap.min.css")
from(bootswatchThemeMinCss) {
rename { "bootstrap.min.css" }
}
into(layout.projectDirectory.dir("src/main/resources/css"))
}
tasks.assemble {
dependsOn(installThemeCss)
}

View file

@ -3,16 +3,16 @@ package dev.adamko.kafkatorio.webmap
import dev.adamko.kafkatorio.schema.packets.EventServerPacket import dev.adamko.kafkatorio.schema.packets.EventServerPacket
import dev.adamko.kafkatorio.webmap.layout.headerNav import dev.adamko.kafkatorio.webmap.layout.headerNav
import dev.adamko.kafkatorio.webmap.layout.homePage import dev.adamko.kafkatorio.webmap.layout.homePage
import dev.adamko.kafkatorio.webmap.layout.serverPage import dev.adamko.kafkatorio.webmap.layout.servers.serverPage
import dev.adamko.kafkatorio.webmap.services.WebsocketService import dev.adamko.kafkatorio.webmap.services.WebsocketService
import io.kvision.Application import io.kvision.Application
import io.kvision.BootstrapCssModule
import io.kvision.BootstrapIconsModule import io.kvision.BootstrapIconsModule
import io.kvision.BootstrapModule import io.kvision.BootstrapModule
import io.kvision.ChartModule import io.kvision.ChartModule
import io.kvision.CoreModule import io.kvision.CoreModule
import io.kvision.FontAwesomeModule import io.kvision.FontAwesomeModule
import io.kvision.html.div import io.kvision.html.div
import io.kvision.html.h3
import io.kvision.html.header import io.kvision.html.header
import io.kvision.html.main import io.kvision.html.main
import io.kvision.module import io.kvision.module
@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.onEach
val rootJob = Job() val rootJob = Job()
class App : Application() { object App : Application() {
private val coroutineScope: CoroutineScope = CoroutineScope( private val coroutineScope: CoroutineScope = CoroutineScope(
Dispatchers.Default Dispatchers.Default
@ -48,15 +48,23 @@ class App : Application() {
private var appState: MutableMap<String, Any> = mutableMapOf() private var appState: MutableMap<String, Any> = mutableMapOf()
var reduxStore: ReduxStore<SiteState, SiteAction> by appState var siteStateStore: ReduxStore<SiteState, SiteAction> by appState
private set private set
// var wsService: WebsocketService by appState // var wsService: WebsocketService by appState
// private set // private set
init { init {
this.reduxStore = createReduxStore(SiteState::plus, SiteState()) this.siteStateStore = createReduxStore(SiteState::plus, SiteState())
// this.wsService = wsService // this.wsService = wsService
require("./css/kafkatorio.css") require("./css/kafkatorio.css")
WebsocketService.packetsFlow
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
.onEach { tileSavedEvent ->
// println("[packetsFlow] triggering tile refresh ${tileSavedEvent.filename.value}")
siteStateStore.dispatch(SiteAction.EventServerUpdate(tileSavedEvent))
// reduxStore.map.refreshUpdatedTilePng(tileSavedEvent.filename)
}.launchIn(coroutineScope)
} }
// private val gameState: FactorioGameState // private val gameState: FactorioGameState
@ -70,23 +78,25 @@ class App : Application() {
this.appState = state.toMutableMap() this.appState = state.toMutableMap()
val siteRouting = SiteRouting(reduxStore) // val siteRouting = SiteRouting(reduxStore)
val websocketService = WebsocketService(reduxStore) // val websocketService = WebsocketService(siteStateStore)
siteRouting.init() SiteRouting.init()
root("kvapp") { root("kvapp") {
header().bind(siteRouting.siteStateStore) { state -> header().bind(siteStateStore) { state ->
headerNav(state) headerNav(state)
} }
div("Kafkatorio Web Map") h3("Kafkatorio Web Map")
main().bind(siteRouting.siteStateStore) { state -> main().bind(siteStateStore) { state ->
when (state.view) { div(className = "container-fluid") {
SiteView.HOME -> homePage(state) when (state.view) {
SiteView.SERVER -> serverPage(state) SiteView.HOME -> homePage(state)
SiteView.SERVER -> serverPage(state)
}
} }
} }
@ -103,14 +113,6 @@ class App : Application() {
// }) // })
// } // }
websocketService.packetsFlow
.filterIsInstance<EventServerPacket.ChunkTileSaved>()
.onEach { tileSavedEvent ->
// println("[packetsFlow] triggering tile refresh ${tileSavedEvent.filename.value}")
reduxStore.dispatch(SiteAction.EventServerUpdate(tileSavedEvent))
// reduxStore.map.refreshUpdatedTilePng(tileSavedEvent.filename)
}.launchIn(coroutineScope)
} }
override fun dispose(): Map<String, Any> { override fun dispose(): Map<String, Any> {
@ -129,14 +131,16 @@ fun main() {
// val wsService = WebsocketService( // val wsService = WebsocketService(
// reduxStore = reduxStore, // reduxStore = reduxStore,
// ) // )
App( // App(
// reduxStore = reduxStore, // reduxStore = reduxStore,
// wsService = wsService, // wsService = wsService,
) // )
App
}, },
module.hot, module.hot,
BootstrapModule, BootstrapModule,
BootstrapCssModule, // BootstrapCssModule,
BootstrapIconsModule, BootstrapIconsModule,
FontAwesomeModule, FontAwesomeModule,
ChartModule, ChartModule,

View file

@ -1,12 +1,13 @@
package dev.adamko.kafkatorio.webmap package dev.adamko.kafkatorio.webmap.config
import kotlinx.browser.window import kotlinx.browser.window
object Props {
object ApplicationProperties {
@Suppress("HttpUrlsUsage") @Suppress("HttpUrlsUsage")
val websocketServerUrl: String val websocketServerUrl: String
get() = window.location.href get() = window.location.origin
.replace("http://", "ws://") .replace("http://", "ws://")
.replace("https://", "wss://") + .replace("https://", "wss://") +
"kafkatorio/ws" "/kafkatorio/ws"
} }

View file

@ -10,7 +10,7 @@ import io.kvision.html.nav
fun Container.headerNav(state: SiteState) { fun Container.headerNav(state: SiteState) {
nav(className = "navbar") { nav(className = "navbar") {
div(className = "container") { div(className = "container") {
link("home", "#${SiteView.HOME.pathMatcher}") link("home", SiteView.HOME.path)
} }
} }
} }

View file

@ -1,10 +1,14 @@
package dev.adamko.kafkatorio.webmap.layout package dev.adamko.kafkatorio.webmap.layout
import dev.adamko.kafkatorio.webmap.SiteRouting
import dev.adamko.kafkatorio.webmap.SiteState import dev.adamko.kafkatorio.webmap.SiteState
import io.kvision.core.Container import io.kvision.core.Container
import io.kvision.html.ButtonSize
import io.kvision.html.ButtonStyle
import io.kvision.html.button
import io.kvision.html.div import io.kvision.html.div
import io.kvision.html.h5 import io.kvision.html.h5
import io.kvision.html.link import io.kvision.html.span
fun Container.homePage(state: SiteState) { fun Container.homePage(state: SiteState) {
@ -18,9 +22,32 @@ private fun Container.servers(state: SiteState) {
div(className = "servers") { div(className = "servers") {
h5("Servers") h5("Servers")
if (state.serverIds != null) { if (state.serverIds != null) {
state.serverIds.forEach { serverId -> div {
div { span("Available servers: ${state.serverIds.size}")
link("server", "/servers/$serverId") }
div {
state.serverIds.forEach { serverId ->
// div {
// link(
// label = "$serverId",
// dataNavigo = true,
// url = "/servers/$serverId",
// ) {
//// onClick {
//// SiteRouting.serverView(serverId)
//// }
// }
// }
button(text = "$serverId") {
style = ButtonStyle.PRIMARY
size = ButtonSize.SMALL
onClick {
SiteRouting.serverView(serverId)
}
}
} }
} }
} else { } else {

View file

@ -1,4 +1,4 @@
package dev.adamko.kafkatorio.webmap.layout package dev.adamko.kafkatorio.webmap.layout.servers
import dev.adamko.kafkatorio.webmap.SiteState import dev.adamko.kafkatorio.webmap.SiteState
import io.kvision.core.Container import io.kvision.core.Container
@ -8,19 +8,23 @@ import io.kvision.maps.externals.leaflet.layer.tile.GridLayer
fun Container.serverPage(state: SiteState) { fun Container.serverPage(state: SiteState) {
val factorioGameState = state.factorioGameState val factorioGameState = state.factorioGameState
div(className = "server-page") { div(className = "server-page") {
if (factorioGameState != null) { if (factorioGameState != null) {
add(factorioGameState.map.kvMap) add(factorioGameState.map.kvMap)
factorioGameState.map.kvMap.leafletMap {
invalidateSize(true)
eachLayer({
if (it is GridLayer<*>) {
it.redraw()
}
})
}
} }
} }
// TODO figure out how to run this after adding kvMap to the container...
// (doing it now causes an NPE because kvMap isn't added to the DOM yet
// factorioGameState?.map?.kvMap?.leafletMap {
// invalidateSize(true)
// eachLayer({
// if (it is GridLayer<*>) {
// it.redraw()
// }
// })
// }
} }

View file

@ -37,8 +37,10 @@ class FactorioMap(
) { ) {
val kvMap: Maps = Maps { val kvMap: Maps = Maps {
width = 800.px minHeight = 800.px
height = 800.px minWidth = 800.px
// width = 100.vh
// height = 100.vh
margin = 10.px margin = 10.px
} }
@ -164,12 +166,5 @@ class FactorioMap(
companion object { companion object {
const val DYNAMIC_RELOAD_ATT = "dynamic-reload" const val DYNAMIC_RELOAD_ATT = "dynamic-reload"
// private val serverName: String?
// get() = window.location.pathname
// .substringAfter("servers")
// .substringBefore("/")
// .ifBlank { null }
} }
} }

View file

@ -9,40 +9,44 @@ import dev.adamko.kafkatorio.webmap.state.FactorioGameState
import io.kvision.navigo.Match import io.kvision.navigo.Match
import io.kvision.navigo.Navigo import io.kvision.navigo.Navigo
import io.kvision.redux.RAction import io.kvision.redux.RAction
import io.kvision.redux.ReduxStore
import io.kvision.routing.routing import io.kvision.routing.routing
import kotlin.coroutines.CoroutineContext
import kotlin.js.RegExp import kotlin.js.RegExp
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SiteRouting( object SiteRouting : CoroutineScope {
val siteStateStore: ReduxStore<SiteState, SiteAction>
) : CoroutineScope by CoroutineScope(Dispatchers.Default + SupervisorJob()) { override val coroutineContext: CoroutineContext =
Dispatchers.Default + SupervisorJob(rootJob) + CoroutineName("SiteRouting")
fun init() { fun init() {
routing routing
.on(SiteView.HOME) { homepageView() } .on(SiteView.HOME) { homepageView() }
.on(SiteView.SERVER) { match -> .on(SiteView.SERVER) { match ->
serverView(match) val serverId = match.data[0] as? String ?: error("invalid server id")
serverView(FactorioServerId(serverId))
} }
.resolve() .resolve()
} }
private fun homepageView() { fun homepageView() {
siteStateStore.dispatch(SiteAction.HomePage) App.siteStateStore.dispatch(SiteAction.HomePage)
launch { launch {
val serverIds = EventsServerClient.serverIds() val serverIds = EventsServerClient.serverIds()
siteStateStore.dispatch(SiteAction.ServerIdsLoaded(serverIds)) App.siteStateStore.dispatch(SiteAction.ServerIdsLoaded(serverIds))
} }
} }
private fun serverView(match: Match) { fun serverView(serverId: FactorioServerId) {
val serverId = match.data[0] as? String ?: error("invalid server id") App.siteStateStore.dispatch(SiteAction.ServerPage(serverId))
siteStateStore.dispatch(SiteAction.ServerPage(FactorioServerId(serverId)))
} }
} }
@ -52,7 +56,10 @@ data class SiteState(
val serverIds: List<FactorioServerId>? = null, val serverIds: List<FactorioServerId>? = null,
val factorioGameState: FactorioGameState? = null, val factorioGameState: FactorioGameState? = null,
) { ) : CoroutineScope {
override val coroutineContext: CoroutineContext =
Dispatchers.Default + SupervisorJob(rootJob) + CoroutineName("SiteState")
operator fun plus(action: SiteAction): SiteState = when (action) { operator fun plus(action: SiteAction): SiteState = when (action) {
is SiteAction.HomePage -> copy(view = SiteView.HOME) is SiteAction.HomePage -> copy(view = SiteView.HOME)
@ -68,9 +75,20 @@ data class SiteState(
factorioGameState = factorioGameState?.let { it + action } factorioGameState = factorioGameState?.let { it + action }
) )
is SiteAction.EventServerUpdate -> copy( is SiteAction.EventServerUpdate -> when (action.packet) {
is EventServerPacket.ChunkTileSaved -> {
if (factorioGameState?.map != null) {
launch {
factorioGameState.map.refreshUpdatedTilePng(action.packet.filename)
}
}
this
}
) is EventServerPacket.Kafkatorio -> {
this
}
}
} }
} }
@ -93,19 +111,18 @@ sealed interface SiteAction : RAction {
sealed interface SiteView { sealed interface SiteView {
val pathMatcher: Any
sealed interface StaticPath : SiteView { sealed interface StaticPath : SiteView {
override val pathMatcher: String val path: String
} }
sealed interface DynamicPath : SiteView { sealed interface DynamicPath : SiteView {
override val pathMatcher: RegExp val pathMatcher: RegExp
} }
object HOME : StaticPath { object HOME : StaticPath {
override val pathMatcher: String = "/" override val path: String = "/"
} }
object SERVER : DynamicPath { object SERVER : DynamicPath {
@ -113,19 +130,15 @@ sealed interface SiteView {
} }
} }
//val SiteView.urlMatcher: String
// get() = when (this) {
// SiteView.HOME -> "/"
// SiteView.SERVER -> "servers/${FactorioServerId.validIdRegex}"
// }
private fun Navigo.on(view: SiteView, handler: (Match) -> Unit): Navigo { private fun Navigo.on(view: SiteView, handler: (Match) -> Unit): Navigo {
return when (view) { return when (view) {
is SiteView.StaticPath -> on(view.pathMatcher, { match: Match -> handler(match) }) is SiteView.StaticPath -> on(view.path, { match: Match -> handler(match) })
is SiteView.DynamicPath -> on(view.pathMatcher, { match: Match -> handler(match) }) is SiteView.DynamicPath -> on(view.pathMatcher, { match: Match -> handler(match) })
} }
} }
//private fun Navigo.on( //private fun Navigo.on(
// view: SiteView, // view: SiteView,
// before: BeforeHook? = null, // before: BeforeHook? = null,

View file

@ -12,12 +12,11 @@ import dev.adamko.kafkatorio.schema.packets.MapChunkTileUpdate
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate
import dev.adamko.kafkatorio.webmap.App
import dev.adamko.kafkatorio.webmap.SiteAction import dev.adamko.kafkatorio.webmap.SiteAction
import dev.adamko.kafkatorio.webmap.SiteState
import dev.adamko.kafkatorio.webmap.config.ApplicationProperties import dev.adamko.kafkatorio.webmap.config.ApplicationProperties
import dev.adamko.kafkatorio.webmap.config.jsonMapper import dev.adamko.kafkatorio.webmap.config.jsonMapper
import dev.adamko.kafkatorio.webmap.rootJob import dev.adamko.kafkatorio.webmap.rootJob
import io.kvision.redux.ReduxStore
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -31,9 +30,7 @@ import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event import org.w3c.dom.events.Event
class WebsocketService( object WebsocketService : CoroutineScope {
private val siteStateStore: ReduxStore<SiteState, SiteAction>
) : CoroutineScope {
override val coroutineContext: CoroutineContext = override val coroutineContext: CoroutineContext =
CoroutineName("WebsocketService") + Job(rootJob) CoroutineName("WebsocketService") + Job(rootJob)
@ -80,7 +77,7 @@ class WebsocketService(
is EventServerPacket.Kafkatorio -> is EventServerPacket.Kafkatorio ->
when (val packetData = packet.packet.data) { when (val packetData = packet.packet.data) {
is PlayerUpdate -> is PlayerUpdate ->
siteStateStore.dispatch( App.siteStateStore.dispatch(
SiteAction.FactorioUpdate.Player(packet.packet.tick, packetData) SiteAction.FactorioUpdate.Player(packet.packet.tick, packetData)
) )

File diff suppressed because it is too large Load diff

View file

@ -7,14 +7,16 @@
image-rendering: pixelated; image-rendering: pixelated;
} }
html { /*html {*/
background: #2B2B2BFF; /* background: #2B2B2BFF;*/
} /*}*/
body { /*body {*/
background: #2B2B2BFF; /* background: #2B2B2BFF;*/
} /*}*/
.leaflet-container { .leaflet-container {
background-color: rgba(255, 0, 0, 0.0); background-color: rgba(255, 0, 0, 0.0) !important;
/*background: rgb(34, 34, 34) !important;*/
/*background: var(--bs-body-bg-rgb) !important;*/
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -1,11 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kafkatorio Web Map</title> <title>Kafkatorio Web Map</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.1.0/dist/superhero/bootstrap.min.css" integrity="sha256-T8h7oW5tHp4MhFhmRoDI8xjnB2ld89qSgc3s9g8T0uU=" crossorigin="anonymous"> <!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.1.0/dist/superhero/bootstrap.min.css" integrity="sha256-T8h7oW5tHp4MhFhmRoDI8xjnB2ld89qSgc3s9g8T0uU=" crossorigin="anonymous">-->
<script type="text/javascript" src="main.bundle.js"></script> <link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/kafkatorio.css">
<link rel="icon" type="image/png" href="/img/kafkatorio-logo.png"/>
<!--suppress HtmlUnknownTarget -->
<script type="text/javascript" src="main.bundle.js"></script>
</head> </head>
<body> <body>
<div id="kvapp"></div> <div id="kvapp"></div>