From 5df0a2578853112443e79439d433e9649f5e460b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mikl=C3=B3s=20T=C3=B3th?= <tothmiklostibor@gmail.com>
Date: Sat, 5 Mar 2022 08:54:27 +0100
Subject: [PATCH] Load from API

---
 mosogepsch/build.gradle.kts                   |   5 +
 mosogepsch/src/jsMain/kotlin/Main.kt          |   1 +
 mosogepsch/src/jsMain/kotlin/api/Api.kt       | 212 +++---------------
 mosogepsch/src/jsMain/kotlin/api/Types.kt     |  21 +-
 .../src/jsMain/kotlin/components/Content.kt   |  20 +-
 .../src/jsMain/kotlin/components/FloorCard.kt |   2 +-
 .../src/jsMain/kotlin/components/Spinner.kt   |  51 +++++
 .../src/jsMain/kotlin/components/UnderCard.kt |  15 +-
 .../src/jsMain/kotlin/components/Utils.kt     |  16 +-
 .../src/jsMain/kotlin/localization/English.kt |   1 +
 .../jsMain/kotlin/localization/Hungarian.kt   |   1 +
 mosogepsch/src/jsMain/kotlin/styles/Style.kt  |   2 +-
 12 files changed, 134 insertions(+), 213 deletions(-)
 create mode 100644 mosogepsch/src/jsMain/kotlin/components/Spinner.kt

diff --git a/mosogepsch/build.gradle.kts b/mosogepsch/build.gradle.kts
index 0ce87ec..a53bac2 100644
--- a/mosogepsch/build.gradle.kts
+++ b/mosogepsch/build.gradle.kts
@@ -35,6 +35,8 @@ kotlin {
 
         val jsMain by getting {
             dependencies {
+                val ktor_version = "1.6.5"
+
                 implementation(compose.web.core)
                 implementation(compose.runtime)
                 implementation("app.softwork:bootstrap-compose:0.0.49")
@@ -42,6 +44,9 @@ kotlin {
                 implementation(npm("@js-joda/timezone", "2.3.0"))
                 implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.1")
                 implementation(npm("no-darkreader", "1.0.1"))
+                implementation("io.ktor:ktor-client-core:$ktor_version")
+                implementation("io.ktor:ktor-client-js:$ktor_version")
+                implementation("io.ktor:ktor-client-serialization:$ktor_version")
             }
         }
         val jsTest by getting {
diff --git a/mosogepsch/src/jsMain/kotlin/Main.kt b/mosogepsch/src/jsMain/kotlin/Main.kt
index f698cd5..a4ca2aa 100644
--- a/mosogepsch/src/jsMain/kotlin/Main.kt
+++ b/mosogepsch/src/jsMain/kotlin/Main.kt
@@ -39,6 +39,7 @@ fun main() {
         Style(mosogepStyle)
         Style(CardStyle)
         Style(FloorStyle)
+        Style(SpinnerStyle)
         CompositionLocalProvider(LocalStyle provides mosogepStyle, LocalLang provides lang) {
             Header {
                 Navbar(
diff --git a/mosogepsch/src/jsMain/kotlin/api/Api.kt b/mosogepsch/src/jsMain/kotlin/api/Api.kt
index 72ecb6b..d88764b 100644
--- a/mosogepsch/src/jsMain/kotlin/api/Api.kt
+++ b/mosogepsch/src/jsMain/kotlin/api/Api.kt
@@ -1,198 +1,46 @@
 package api
 
+import io.ktor.client.*
+import io.ktor.client.engine.js.*
+import io.ktor.client.features.json.*
+import io.ktor.client.features.json.serializer.*
+import io.ktor.client.request.*
+import io.ktor.client.response.*
+import kotlinx.browser.window
+import kotlinx.coroutines.delay
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
 import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
 
 
 object Api {
+    val client = HttpClient(Js) {
+        install(JsonFeature) { serializer = KotlinxSerializer() }
+    }
+
     private val json = Json {
         ignoreUnknownKeys = true
     }
 
-    @OptIn(ExperimentalSerializationApi::class)
-    fun getOnce(): Response {
-        val jsonStr = """        
-{
-  "floors": [
-    {
-      "id": 13,
-      "machines": [
-        {
-          "id": 1301,
-          "kindOf": "Dryer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538897
-        },
-        {
-          "id": 1302,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538897
-        }
-      ]
-    },
-    {
-      "id": 3,
-      "machines": [
-        {
-          "id": 301,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538966
-        },
-        {
-          "id": 302,
-          "kindOf": "Washer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538966
-        }
-      ]
-    },
-    {
-      "id": 15,
-      "machines": [
-        {
-          "id": 1501,
-          "kindOf": "Dryer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538951
-        },
-        {
-          "id": 1502,
-          "kindOf": "Washer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538951
-        }
-      ]
-    },
-    {
-      "id": 17,
-      "machines": [
-        {
-          "id": 1701,
-          "kindOf": "Dryer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538804
-        },
-        {
-          "id": 1702,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538804
-        }
-      ]
-    },
-    {
-      "id": 2,
-      "machines": [
-        {
-          "id": 201,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:37 UTC 2022",
-          "unixTimeStamp": 1645102537554
-        },
-        {
-          "id": 202,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:37 UTC 2022",
-          "unixTimeStamp": 1645102537555
-        }
-      ]
-    },
-    {
-      "id": 9,
-      "machines": [
-        {
-          "id": 901,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538994
-        },
-        {
-          "id": 902,
-          "kindOf": "Washer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538994
-        }
-      ]
-    },
-    {
-      "id": 7,
-      "machines": [
-        {
-          "id": 701,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538998
-        },
-        {
-          "id": 702,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538998
-        }
-      ]
-    },
-    {
-      "id": 11,
-      "machines": [
-        {
-          "id": 1101,
-          "kindOf": "Dryer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538516
-        },
-        {
-          "id": 1102,
-          "kindOf": "Washer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538516
-        }
-      ]
-    },
-    {
-      "id": 10,
-      "machines": [
-        {
-          "id": 1001,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:37 UTC 2022",
-          "unixTimeStamp": 1645102537429
-        },
-        {
-          "id": 1002,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:37 UTC 2022",
-          "unixTimeStamp": 1645102537429
-        }
-      ]
+    var lastRequestTime: Instant = Instant.DISTANT_PAST
+        private set
+
+    private val url = if (window.location.host.contains("localhost")) {
+        "https://mosogep-ng.sch.bme.hu/api/v2"
+    } else {
+        "/api/v2"
     }
-  ]
-}
-    """.trimIndent()
-        val dec = json.decodeFromString<Response>(jsonStr)
-        val floors = dec.floors.map { floor ->
+
+    @OptIn(ExperimentalSerializationApi::class)
+    suspend fun getOnce(): Response {
+        lastRequestTime = Clock.System.now()
+
+        val resp: Response = client.request(url)
+
+        val floors = resp.floors.map { floor ->
             floor.copy(machines = floor.machines.sortedBy { it.kindOf })
         }.sortedBy { it.id }
-        return dec.copy(floors = floors)
+        return resp.copy(floors = floors)
     }
 }
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/api/Types.kt b/mosogepsch/src/jsMain/kotlin/api/Types.kt
index 3388dee..af7e945 100644
--- a/mosogepsch/src/jsMain/kotlin/api/Types.kt
+++ b/mosogepsch/src/jsMain/kotlin/api/Types.kt
@@ -1,14 +1,7 @@
 package api
 
 import kotlinx.datetime.Instant
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
-import kotlinx.serialization.descriptors.PrimitiveKind
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
 
 @Serializable
 data class Response(
@@ -26,10 +19,8 @@ data class Machine(
     val id: Int,
     val kindOf: MachineKind,
     var status: MachineStatus,
-
-    @SerialName("unixTimeStamp")
-    @Serializable(with = UnixDateSerializer::class)
     var lastQueryTime: Instant,
+    var lastChanged: Instant,
 )
 
 @Serializable
@@ -37,10 +28,8 @@ enum class MachineKind { Dryer, Washer }
 @Serializable
 enum class MachineStatus { NotAvailable, Available }
 
-object UnixDateSerializer : KSerializer<Instant> {
-    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.LONG)
-
-    override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeLong(value.epochSeconds)
-
-    override fun deserialize(decoder: Decoder): Instant = Instant.fromEpochMilliseconds(decoder.decodeLong())
+@OptIn(kotlin.time.ExperimentalTime::class)
+val Machine.isLostInSpace: Boolean get() {
+    val diff = Api.lastRequestTime - lastQueryTime
+    return diff.inWholeMinutes > 10
 }
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/components/Content.kt b/mosogepsch/src/jsMain/kotlin/components/Content.kt
index 472beb7..a755213 100644
--- a/mosogepsch/src/jsMain/kotlin/components/Content.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/Content.kt
@@ -3,15 +3,23 @@ package components
 import androidx.compose.runtime.*
 import api.Api
 import api.Machine
+import api.Response
 import app.softwork.bootstrapcompose.Container
+import kotlinx.coroutines.launch
 import org.jetbrains.compose.web.css.*
 import org.jetbrains.compose.web.dom.Div
+import org.jetbrains.compose.web.dom.Text
 
 @Composable
 fun content() {
-    val data by remember { mutableStateOf(Api.getOnce()) }
+    var data by remember { mutableStateOf<Response?>(null) }
     var machine by remember { mutableStateOf<Machine?>(null) }
 
+    val scope = rememberCoroutineScope()
+    LaunchedEffect(Unit) {
+        scope.launch { data = Api.getOnce() }
+    }
+
     Container(attrs = {
         style {
             display(DisplayStyle.Flex)
@@ -21,9 +29,13 @@ fun content() {
             paddingTop(1.em)
         }
     }) {
-        data.floors.forEach {
-            floor(it, machine) { m ->
-                machine = m
+        if (data == null ) {
+            spinner()
+        } else {
+            data!!.floors.forEach {
+                floor(it, machine) { m ->
+                    machine = m
+                }
             }
         }
     }
diff --git a/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt b/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
index b37c80a..e6c4351 100644
--- a/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
@@ -25,7 +25,7 @@ fun FloorCard(floor: Floor, selectedMachine: Machine?, setMachine: (Machine?)->U
             classes(FloorStyle.floorNum)
         }) { Text("${floor.id}.") }
         floor.machines.forEach { machine ->
-            val cardColor = machine.status.toColor()
+            val cardColor = machine.toColor()
             card(attrs = {
                 classes(FloorStyle.machine)
                 style {
diff --git a/mosogepsch/src/jsMain/kotlin/components/Spinner.kt b/mosogepsch/src/jsMain/kotlin/components/Spinner.kt
new file mode 100644
index 0000000..bd9adbc
--- /dev/null
+++ b/mosogepsch/src/jsMain/kotlin/components/Spinner.kt
@@ -0,0 +1,51 @@
+package components
+
+import androidx.compose.runtime.Composable
+import org.jetbrains.compose.web.ExperimentalComposeWebApi
+import org.jetbrains.compose.web.css.*
+import org.jetbrains.compose.web.dom.Div
+import styles.ColorPair
+import styles.LocalStyle
+
+@OptIn(ExperimentalComposeWebApi::class)
+object SpinnerStyle: StyleSheet() {
+    val spin by keyframes {
+        from {
+            transform { rotate(0.deg) }
+        }
+        to {
+            transform { rotate(360.deg) }
+        }
+    }
+    val spinner by style {
+        borderRadius(50.percent)
+        property("border", ".5em solid #f3f3f3")
+        property("border-top", ".5em solid #3498db")
+        width(3.em)
+        height(3.em)
+        animation(spin) {
+            duration(1.s)
+            timingFunction(AnimationTimingFunction.Linear)
+            iterationCount(null)
+        }
+    }
+}
+
+@Composable
+fun spinner() {
+    val dark = LocalStyle.current.dark
+    val colors = LocalStyle.current.colors
+
+    val color = ColorPair(
+        bg = if (dark) colors.onPrimary else colors.surfaceVariant,
+        fg = colors.primary
+    )
+
+    Div(attrs = {
+        classes(SpinnerStyle.spinner)
+        style {
+            property("border", ".5em solid ${color.bg}")
+            property("border-top", ".5em solid ${color.fg}")
+        }
+    })
+}
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/components/UnderCard.kt b/mosogepsch/src/jsMain/kotlin/components/UnderCard.kt
index aa67567..c523856 100644
--- a/mosogepsch/src/jsMain/kotlin/components/UnderCard.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/UnderCard.kt
@@ -19,9 +19,8 @@ import styles.applyColorPair
 @Composable
 fun underCard(floor: Floor, machine: Machine, selectedMachine: Machine?) {
     val lang = LocalLang.current
-    val cardColor = machine.status.toUnderColor()
-    val subbedColor = subbedColor()
-    val unsubbedColor = unsubbedColor()
+    val cardColor = machine.toUnderColor()
+    val btnColor = machine.toColor()
 
     card(attrs = {
         classes(FloorStyle.underflowCard)
@@ -34,17 +33,23 @@ fun underCard(floor: Floor, machine: Machine, selectedMachine: Machine?) {
     }) {
         H5 { Text("${lang.machineKind(machine.kindOf)} ${lang.floorFormat(floor.id)}") }
         P { Text("${lang.lastUpdated} ${lang.formatInstant(machine.lastQueryTime)}") }
+        P { Text("${lang.lastChanged} ${lang.formatInstant(machine.lastChanged)}") }
         P { Text(lang.machineStatus(machine.status)) }
         var subbed by remember { mutableStateOf(false) }
         card(attrs = {
             classes(FloorStyle.subBtn)
             onClick { subbed = !subbed }
             style {
-                applyColorPair(if (subbed) subbedColor else unsubbedColor)
+                applyColorPair(if (!subbed) btnColor else btnColor.invert())
             }
         }) {
             // TODO place icon
             Text(if (subbed) lang.unsub else lang.sub)
         }
     }
-}
\ No newline at end of file
+}
+
+fun ColorPair.invert(): ColorPair = ColorPair(
+    bg = fg,
+    fg = bg,
+)
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/components/Utils.kt b/mosogepsch/src/jsMain/kotlin/components/Utils.kt
index 187121d..d3c30a3 100644
--- a/mosogepsch/src/jsMain/kotlin/components/Utils.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/Utils.kt
@@ -1,24 +1,32 @@
 package components
 
 import androidx.compose.runtime.Composable
+import api.Machine
 import api.MachineKind
 import api.MachineStatus
+import api.isLostInSpace
 import styles.ColorPair
 import styles.LocalStyle
 
 @Composable
-fun MachineStatus.toColor(): ColorPair {
+fun Machine.toColor(): ColorPair {
     val colors = LocalStyle.current.colors
-    return when (this) {
+    if (isLostInSpace) {
+        return ColorPair(colors.background, colors.onBackground)
+    }
+    return when (status) {
         MachineStatus.Available -> ColorPair(colors.primary, colors.onPrimary)
         MachineStatus.NotAvailable -> ColorPair(colors.error, colors.onError)
     }
 }
 
 @Composable
-fun MachineStatus.toUnderColor(): ColorPair {
+fun Machine.toUnderColor(): ColorPair {
     val colors = LocalStyle.current.colors
-    return when (this) {
+    if (isLostInSpace) {
+        return ColorPair(colors.surfaceVariant, colors.onSurfaceVariant)
+    }
+    return when (status) {
         MachineStatus.Available -> ColorPair(colors.primaryContainer, colors.onPrimaryContainer)
         MachineStatus.NotAvailable -> ColorPair(colors.errorContainer, colors.onErrorContainer)
     }
diff --git a/mosogepsch/src/jsMain/kotlin/localization/English.kt b/mosogepsch/src/jsMain/kotlin/localization/English.kt
index 01faadf..f420af1 100644
--- a/mosogepsch/src/jsMain/kotlin/localization/English.kt
+++ b/mosogepsch/src/jsMain/kotlin/localization/English.kt
@@ -15,6 +15,7 @@ abstract class Localization {
     open val notAvailable = "Currently in use"
 
     open val lastUpdated = "Last updated"
+    open val lastChanged = "Last changed"
     open val daysAgo = "days ago"
 
     open val sub = "Subscribe"
diff --git a/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt b/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
index f91015d..f68ed56 100644
--- a/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
+++ b/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
@@ -10,6 +10,7 @@ class Hungarian: Localization() {
     override val notAvailable = "Jelenleg használatban"
 
     override val lastUpdated = "Utoljára frissítve"
+    override val lastChanged = "Utoljára megváltozott"
     override val daysAgo = "napja"
 
     override val sub = "Feliratkozás"
diff --git a/mosogepsch/src/jsMain/kotlin/styles/Style.kt b/mosogepsch/src/jsMain/kotlin/styles/Style.kt
index 20199d2..3d2ddae 100644
--- a/mosogepsch/src/jsMain/kotlin/styles/Style.kt
+++ b/mosogepsch/src/jsMain/kotlin/styles/Style.kt
@@ -8,7 +8,7 @@ class Style(val dark: Boolean = false): StyleSheet() {
     val colors = if (dark) DarkThemeColors else LightThemeColors
 
     companion object {
-        val baseTransition = "background-color 0.75s, color 0.75s"
+        val baseTransition = "background-color 0.75s, color 0.75s, border 0.75s, border-top 0.75s"
         private val footHeight = 4.em
     }
 
-- 
GitLab