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