From d17f9ba3cf9d2fd6adaf1aae4a78a0b3a13edbb9 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 18:41:43 +0100
Subject: [PATCH] Add offline notifier

---
 mosogepsch/src/jsMain/kotlin/Main.kt          | 71 ++-------------
 mosogepsch/src/jsMain/kotlin/api/Api.kt       |  3 +-
 .../src/jsMain/kotlin/components/AppBar.kt    | 90 +++++++++++++++++++
 .../src/jsMain/kotlin/components/Content.kt   | 36 +++++---
 .../src/jsMain/kotlin/components/Error.kt     | 48 ++++++++++
 .../src/jsMain/kotlin/components/FloorCard.kt |  1 -
 .../src/jsMain/kotlin/components/Offline.kt   | 45 ++++++++++
 .../src/jsMain/kotlin/localization/English.kt |  4 +
 .../jsMain/kotlin/localization/Hungarian.kt   |  2 +
 mosogepsch/src/jsMain/kotlin/styles/Style.kt  |  5 +-
 mosogepsch/src/jsMain/resources/offline.svg   |  1 +
 11 files changed, 224 insertions(+), 82 deletions(-)
 create mode 100644 mosogepsch/src/jsMain/kotlin/components/AppBar.kt
 create mode 100644 mosogepsch/src/jsMain/kotlin/components/Error.kt
 create mode 100644 mosogepsch/src/jsMain/kotlin/components/Offline.kt
 create mode 100644 mosogepsch/src/jsMain/resources/offline.svg

diff --git a/mosogepsch/src/jsMain/kotlin/Main.kt b/mosogepsch/src/jsMain/kotlin/Main.kt
index 0733e18..b9bda91 100644
--- a/mosogepsch/src/jsMain/kotlin/Main.kt
+++ b/mosogepsch/src/jsMain/kotlin/Main.kt
@@ -35,6 +35,7 @@ fun main() {
 
     var mosogepStyle by mutableStateOf(Style(dark))
     var lang by mutableStateOf<Localization>(Hungarian())
+    var offline by mutableStateOf(false)
     renderComposable(rootElementId = "root") {
         Style(mosogepStyle)
         Style(CardStyle)
@@ -43,63 +44,10 @@ fun main() {
         Style(SpinnerStyle)
         CompositionLocalProvider(LocalStyle provides mosogepStyle, LocalLang provides lang) {
             Header {
-                Navbar(
-                    attrs = {
-                        style {
-                            backgroundColor(mosogepStyle.colors.surface)
-                            color(mosogepStyle.colors.onSurface)
-                            property("box-shadow", "0 0 .7em #00000033")
-                        }
-                    },
-                    placement = NavbarPlacement.StickyTop,
-                    collapseBehavior = NavbarCollapseBehavior.AtBreakpoint(Breakpoint.Large),
-                    colorScheme = Color.Dark,
-                ) {
-                    DomSideEffect {
-                        it.setImportantBg(mosogepStyle.colors.surface)
-                    }
-                    Div(
-                        attrs = {
-                            style {
-                                display(DisplayStyle.Flex)
-                                flexDirection(FlexDirection.Row)
-                                gap(.5.em)
-                                alignItems(AlignItems.Center)
-                            }
-                        }
-                    ) {
-                        icon(
-                            src = MachineKind.Washer.toIcon(),
-                            color = mosogepStyle.colors.onSurface,
-                            width = 1.5.em
-                        )
-                        Text("MosógépSCH")
-                    }
-                    Div(
-                        attrs = { classes(mosogepStyle.options) }
-                    ) {
-                        switch(
-                            label = "Sötét téma",
-                            value = mosogepStyle.dark,
-                            onSet = {
-                                mosogepStyle = Style(it)
-                                localStorage["darkMode"] = "$it"
-                            }
-                        )
-                        // todo make this a dropdown menu
-                        switch(
-                            label = "magyar",
-                            value = lang is Hungarian,
-                            onSet = {
-                                if (lang is Hungarian) {
-                                    lang = English()
-                                } else {
-                                    lang = Hungarian()
-                                }
-                            }
-                        )
-                    }
-                }
+                appBar(
+                    setStyle = { mosogepStyle = it },
+                    setLang = { lang = it },
+                )
             }
             Main(attrs = {
                 classes(mosogepStyle.content)
@@ -107,7 +55,8 @@ fun main() {
                     backgroundColor(mosogepStyle.colors.background)
                 }
             }) {
-                content()
+                offline(offline)
+                content() { offline = it }
             }
             Footer(
                 attrs = { classes("mt-auto", mosogepStyle.footer) }
@@ -116,10 +65,4 @@ fun main() {
             }
         }
     }
-}
-
-@NoLiveLiterals
-private fun HTMLDivElement.setImportantBg(color: CSSColorValue) {
-    val alma: dynamic = parentElement
-    alma.style.setProperty("background-color", color.toString(), "important")
 }
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/api/Api.kt b/mosogepsch/src/jsMain/kotlin/api/Api.kt
index 12c605b..56e1775 100644
--- a/mosogepsch/src/jsMain/kotlin/api/Api.kt
+++ b/mosogepsch/src/jsMain/kotlin/api/Api.kt
@@ -32,9 +32,8 @@ object Api {
 
     @OptIn(ExperimentalSerializationApi::class)
     suspend fun getOnce(): Response {
-        lastRequestTime = Clock.System.now()
-
         val resp: Response = client.request(url)
+        lastRequestTime = Clock.System.now()
 
         val floors = resp.floors.map { floor ->
             floor.copy(machines = floor.machines.sortedBy { it.kindOf })
diff --git a/mosogepsch/src/jsMain/kotlin/components/AppBar.kt b/mosogepsch/src/jsMain/kotlin/components/AppBar.kt
new file mode 100644
index 0000000..5a4f875
--- /dev/null
+++ b/mosogepsch/src/jsMain/kotlin/components/AppBar.kt
@@ -0,0 +1,90 @@
+package components
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NoLiveLiterals
+import app.softwork.bootstrapcompose.*
+import app.softwork.bootstrapcompose.Color
+import kotlinx.browser.localStorage
+import localization.English
+import localization.Hungarian
+import localization.LocalLang
+import localization.Localization
+import org.jetbrains.compose.web.css.*
+import org.jetbrains.compose.web.dom.Div
+import org.jetbrains.compose.web.dom.Text
+import org.w3c.dom.HTMLDivElement
+import org.w3c.dom.set
+import styles.LocalStyle
+import styles.Style
+
+@Composable
+fun appBar(setStyle: (Style) -> Unit, setLang: (Localization) -> Unit) {
+    val mosogepStyle = LocalStyle.current
+    val lang = LocalLang.current
+    Navbar(
+        attrs = {
+            style {
+                backgroundColor(mosogepStyle.colors.surface)
+                color(mosogepStyle.colors.onSurface)
+                property("box-shadow", "0 0 .7em #00000033")
+            }
+        },
+        placement = NavbarPlacement.StickyTop,
+        collapseBehavior = NavbarCollapseBehavior.AtBreakpoint(Breakpoint.Large),
+        colorScheme = Color.Dark,
+    ) {
+        DomSideEffect {
+            it.setImportantBg(mosogepStyle.colors.surface)
+        }
+        Div(
+            attrs = {
+                style {
+                    display(DisplayStyle.Flex)
+                    flexDirection(FlexDirection.Row)
+                    gap(.5.em)
+                    alignItems(AlignItems.Center)
+                }
+            }
+        ) {
+            icon(
+                src = api.MachineKind.Washer.toIcon(),
+                color = mosogepStyle.colors.onSurface,
+                width = 1.5.em
+            )
+            Text("MosógépSCH")
+
+        }
+        Div(
+            attrs = { classes(mosogepStyle.options) }
+        ) {
+            switch(
+                label = "Sötét téma",
+                value = mosogepStyle.dark,
+                onSet = {
+                    setStyle(styles.Style(it))
+                    localStorage["darkMode"] = "$it"
+                }
+            )
+            // todo make this a dropdown menu
+            switch(
+                label = "magyar",
+                value = lang is Hungarian,
+                onSet = {
+                    setLang(
+                        if (lang is Hungarian) {
+                            English()
+                        } else {
+                            Hungarian()
+                        }
+                    )
+                }
+            )
+        }
+    }
+}
+
+@NoLiveLiterals
+private fun HTMLDivElement.setImportantBg(color: CSSColorValue) {
+    val alma: dynamic = parentElement
+    alma.style.setProperty("background-color", color.toString(), "important")
+}
\ 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 eb303af..a6b77d5 100644
--- a/mosogepsch/src/jsMain/kotlin/components/Content.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/Content.kt
@@ -9,13 +9,13 @@ import kotlinx.coroutines.delay
 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() {
+fun content(offline: (Boolean) -> Unit = {}) {
     var data by remember { mutableStateOf<Response?>(null) }
     var machine by remember { mutableStateOf<Machine?>(null) }
     val subscriptions = remember { mutableStateListOf<Machine>() }
+    var error by remember { mutableStateOf<Throwable?>(null) }
 
     val scope = rememberCoroutineScope()
     LaunchedEffect(Unit) {
@@ -24,14 +24,21 @@ fun content() {
             while (true) {
                 // todo optimize
                 delay(5000)
-                data = Api.getOnce()
-                val machines = data!!.floors.map{ it.machines }.flatten()
-                val nextMachine = machines.find { it.id == machine?.id }
-                machine = nextMachine
-                subscriptions.forEachIndexed { i, machine ->
-                    val newMachine = machines.find { it.id == machine.id }
-                        ?: return@forEachIndexed // skip if not found
-                    subscriptions[i] = newMachine
+                try {
+                    data = Api.getOnce()
+                    val machines = data!!.floors.map { it.machines }.flatten()
+                    val nextMachine = machines.find { it.id == machine?.id }
+                    machine = nextMachine
+                    subscriptions.forEachIndexed { i, machine ->
+                        val newMachine = machines.find { it.id == machine.id }
+                            ?: return@forEachIndexed // skip if not found
+                        subscriptions[i] = newMachine
+                    }
+                    error = null
+                    offline(false)
+                } catch (e: dynamic) {
+                    error = if (e is Throwable) e else Error(e.toString())
+                    if (error?.isNetwork == true) { offline(true) }
                 }
             }
         }
@@ -44,12 +51,13 @@ fun content() {
             flexWrap(FlexWrap.Wrap)
             justifyContent(JustifyContent.Center)
             paddingTop(1.em)
+            position(Position.Relative)
         }
     }) {
-        if (data == null ) {
-            spinner()
-        } else {
-            data!!.floors.forEach {
+        when {
+            error != null && data == null -> errorScreen(error!!)
+            data == null -> spinner()
+            else -> data!!.floors.forEach {
                 floor(it, machine, subscriptions) { m ->
                     machine = m
                 }
diff --git a/mosogepsch/src/jsMain/kotlin/components/Error.kt b/mosogepsch/src/jsMain/kotlin/components/Error.kt
new file mode 100644
index 0000000..7fda70f
--- /dev/null
+++ b/mosogepsch/src/jsMain/kotlin/components/Error.kt
@@ -0,0 +1,48 @@
+package components
+
+import androidx.compose.runtime.*
+import localization.LocalLang
+import org.jetbrains.compose.web.css.*
+import org.jetbrains.compose.web.dom.*
+import styles.LocalStyle
+
+external class TypeError: Throwable
+
+@Composable
+fun errorScreen(e: Throwable) {
+    val colors = LocalStyle.current.colors
+    val lang = LocalLang.current
+    console.log(e)
+
+    var clickCnt by remember { mutableStateOf(0) }
+
+    Div(
+        attrs = {
+            style {
+                display(DisplayStyle.Flex)
+                flexDirection(FlexDirection.Column)
+                gap(1.em)
+                color(colors.onBackground)
+                alignItems(AlignItems.Center)
+            }
+        }
+    ) {
+        H2(attrs = {
+            onClick { clickCnt++ }
+        }) { Text(lang.error) }
+        if (e.isNetwork) {
+            H4 { Text(lang.noConn) }
+        }
+        Div(attrs = {
+            style {
+                if (clickCnt < 5) {
+                    display(DisplayStyle.None)
+                }
+            }
+        }) {
+            Text(e.toString())
+        }
+    }
+}
+
+val Throwable.isNetwork get() = (cause as? TypeError)?.message?.contains("NetworkError") ?: false
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt b/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
index 8b62174..a38dae9 100644
--- a/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
@@ -18,7 +18,6 @@ object FloorCardStyle: StyleSheet() {
         padding(0.5.em)
         bottom(0.px)
         right(0.px)
-        property("transition", "${styles.Style.baseTransition}, opacity 0.75s")
     }
     val machine by style {
         position(Position.Relative)
diff --git a/mosogepsch/src/jsMain/kotlin/components/Offline.kt b/mosogepsch/src/jsMain/kotlin/components/Offline.kt
new file mode 100644
index 0000000..c3ae83d
--- /dev/null
+++ b/mosogepsch/src/jsMain/kotlin/components/Offline.kt
@@ -0,0 +1,45 @@
+package components
+
+import androidx.compose.runtime.Composable
+import localization.LocalLang
+import org.jetbrains.compose.web.css.*
+import org.jetbrains.compose.web.dom.Div
+import org.jetbrains.compose.web.dom.Text
+import styles.LocalStyle
+
+@Composable
+fun offline(offline: Boolean) {
+    val lang = LocalLang.current
+    val colors = LocalStyle.current.colors
+
+    Div( attrs = {
+        style {
+            display(DisplayStyle.Flex)
+            flexDirection(FlexDirection.Row)
+            width(100.percent)
+            alignItems(AlignItems.Center)
+            justifyContent(JustifyContent.Center)
+            overflow("clip")
+            property("transition", "${styles.Style.baseTransition}, max-height 0.75s")
+            if (!offline) {
+                maxHeight(0.px)
+            } else {
+                maxHeight(5.em)
+            }
+        }
+    }) {
+        Div(attrs = {
+            style {
+                padding(1.em)
+                display(DisplayStyle.Flex)
+                flexDirection(FlexDirection.Row)
+                gap(.5.em)
+                alignItems(AlignItems.Center)
+                color(colors.onSurface)
+            }
+        }) {
+            icon("offline.svg", colors.onSurface, 1.25.em)
+            Text(lang.offline)
+        }
+    }
+}
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/localization/English.kt b/mosogepsch/src/jsMain/kotlin/localization/English.kt
index f420af1..86a659d 100644
--- a/mosogepsch/src/jsMain/kotlin/localization/English.kt
+++ b/mosogepsch/src/jsMain/kotlin/localization/English.kt
@@ -35,6 +35,10 @@ abstract class Localization {
         }
         return "${diff.inWholeDays} $daysAgo"
     }
+
+    open val error = "An error occured"
+    open val noConn = "Cannot load data"
+    open val offline = "Offline"
 }
 
 class English: Localization()
diff --git a/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt b/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
index f68ed56..ae38be0 100644
--- a/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
+++ b/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
@@ -25,4 +25,6 @@ class Hungarian: Localization() {
         return "$nevelo $floor. emeleten"
     }
 
+    override val error = "Hiba történt"
+    override val noConn = "Nem sikerült az adatokat frissíteni"
 }
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/styles/Style.kt b/mosogepsch/src/jsMain/kotlin/styles/Style.kt
index 84cba8f..1301056 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, border 0.75s, border-top 0.75s"
+        val baseTransition = "background-color 0.75s, color 0.75s, border 0.75s, border-top 0.75s, opacity 0.75s"
         private val footHeight = 4.em
     }
 
@@ -31,6 +31,9 @@ class Style(val dark: Boolean = false): StyleSheet() {
     }
 
     val content by style {
+        display(DisplayStyle.Flex)
+        flexDirection(FlexDirection.Column)
+        alignItems(AlignItems.FlexStart)
         flexGrow(1)
     }
     val footer by style {
diff --git a/mosogepsch/src/jsMain/resources/offline.svg b/mosogepsch/src/jsMain/resources/offline.svg
new file mode 100644
index 0000000..691bc1e
--- /dev/null
+++ b/mosogepsch/src/jsMain/resources/offline.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M24 15c0-2.64-2.05-4.78-4.65-4.96C18.67 6.59 15.64 4 12 4c-1.33 0-2.57.36-3.65.97l1.49 1.49C10.51 6.17 11.23 6 12 6c3.04 0 5.5 2.46 5.5 5.5v.5H19c1.66 0 3 1.34 3 3 0 .99-.48 1.85-1.21 2.4l1.41 1.41c1.09-.92 1.8-2.27 1.8-3.81zM4.41 3.86L3 5.27l2.77 2.77h-.42C2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h11.73l2 2 1.41-1.41L4.41 3.86zM6 18c-2.21 0-4-1.79-4-4s1.79-4 4-4h1.73l8 8H6z"/></svg>
\ No newline at end of file
-- 
GitLab