diff --git a/mosogepsch/build.gradle.kts b/mosogepsch/build.gradle.kts index 0ce87ec459c91cac481376f1eba18cba054ae013..a53bac27b903776928e91d3646726aab60988890 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 f698cd5d3678c205c108e3d88410df44e26acd0d..a4ca2aa90118c6b86ba27faa9bf1a042a52eb620 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 72ecb6bf62d5d48ae8fcb954aa909873f07603a1..d88764b4c341e243dd7eb000c87872c8d0cec5e0 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 3388deed41cbdfdd3847e3b7bafca7773158159f..af7e945076be9f195173bf1542cd3d87dd516bf1 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 472beb7e2475660e1abcd7dc09acd74a85624710..a755213c896c753409c309d1ed9ec974b015b97e 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 b37c80aaf3e6935cf387511473f2cab17f2ca0e6..e6c43510bb1fed3ea4a344fea24752ebe775ce33 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 0000000000000000000000000000000000000000..bd9adbc73994c4b983d1bb167b5284197815df3f --- /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 aa675675ff8c87425be0e8341c86493800f7d609..c5238565bd3a5380a33ebec38c5597709d4e0901 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 187121d7231e2e671919b819f3a3c0953d496b32..d3c30a329bf03f68751be94ce44424e44b90463f 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 01faadf3f0b0e9ffc1e6f4fc0fb00b746012c4cc..f420af110e477370fb8e62774bce3abd5fb9485b 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 f91015df24095f55e88dceb19532251a67a1a9a5..f68ed56f735d3b2339af433eef5bd83874e22cb0 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 20199d2666cf580191e04eb28381e76d151132e6..3d2ddae75daca08a68bcf5e58402bfaaeeddb3e8 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 }