From 4e15673396860151c14e6e769272df416cbd9f98 Mon Sep 17 00:00:00 2001
From: Janne Mareike Koschinski <mail@justjanne.de>
Date: Tue, 14 Nov 2023 23:32:21 +0100
Subject: [PATCH] wip: progress

---
 .../de/chaosdorf/meteroid/sync/SyncManager.kt | 15 +++++++++
 .../de/chaosdorf/meteroid/ui/AppRouter.kt     |  2 +-
 .../meteroid/ui/drinks/DrinkListScreen.kt     | 19 +++++++++++
 .../meteroid/ui/drinks/DrinkListViewModel.kt  |  5 +++
 .../meteroid/ui/money/MoneyListScreen.kt      | 19 +++++++++++
 .../meteroid/ui/money/MoneyListViewModel.kt   |  5 +++
 .../meteroid/ui/servers/AddServerViewModel.kt | 24 ++------------
 .../ui/transactions/PurchaseListScreen.kt     | 26 ++++++++++++---
 .../ui/transactions/PurchaseViewModel.kt      |  9 ++++-
 ...tension.kt => MeteApiFactoryExtensions.kt} | 33 ++++++++++++++-----
 10 files changed, 121 insertions(+), 36 deletions(-)
 rename app/src/main/kotlin/de/chaosdorf/meteroid/util/{PwaManifestExtension.kt => MeteApiFactoryExtensions.kt} (65%)

diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt
index bb82525..c217089 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt
@@ -29,16 +29,31 @@ import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.ServerRepository
 import de.chaosdorf.meteroid.model.User
+import de.chaosdorf.meteroid.util.newServer
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import java.math.BigDecimal
 import javax.inject.Inject
 
 class SyncManager @Inject constructor(
   private val factory: MeteApiFactory,
+  private val serverRepository: ServerRepository,
   private val userSyncHandler: UserSyncHandler,
   private val drinkSyncHandler: DrinkSyncHandler,
   private val transactionSyncHandler: TransactionSyncHandler
 ) {
+  suspend fun checkOffline(server: Server): Boolean {
+    val updated = factory.newServer(server.serverId, server.url)
+    return if (updated == null) {
+      true
+    } else {
+      serverRepository.save(updated)
+      false
+    }
+  }
+
   suspend fun sync(server: Server, user: User?) {
     try {
       userSyncHandler.sync(server)
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
index f7237af..a43e4ca 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt
@@ -43,7 +43,7 @@ import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.navigation
 import androidx.navigation.compose.rememberNavController
-import de.chaosdorf.meteroid.ui.Transactions.TransactionListScreen
+import de.chaosdorf.meteroid.ui.transactions.TransactionListScreen
 import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen
 import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel
 import de.chaosdorf.meteroid.ui.money.MoneyListScreen
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
index b699100..6604ee7 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt
@@ -34,7 +34,11 @@ import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
@@ -63,6 +67,18 @@ fun DrinkListScreen(
   val account by viewModel.account.collectAsState()
   val drinks by viewModel.drinks.collectAsState()
   val filters by viewModel.filters.collectAsState()
+  val snackbarHostState = remember { SnackbarHostState() }
+
+  LaunchedEffect(account) {
+    val offline = viewModel.checkOffline(account?.server)
+    snackbarHostState.currentSnackbarData?.dismiss()
+    if (offline) {
+      snackbarHostState.showSnackbar(
+        message = "Unable to connect to server",
+        duration = SnackbarDuration.Indefinite
+      )
+    }
+  }
 
   Scaffold(
     topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) },
@@ -72,6 +88,9 @@ fun DrinkListScreen(
         historyEnabled = account?.user?.audit == true,
         navigateTo = onNavigate
       )
+    },
+    snackbarHost = {
+      SnackbarHost(hostState = snackbarHostState)
     }
   ) { paddingValues: PaddingValues ->
     Column(Modifier.padding(paddingValues)) {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
index 162323d..e56a782 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt
@@ -31,6 +31,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.model.Drink
 import de.chaosdorf.meteroid.model.DrinkRepository
+import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.sync.AccountProvider
 import de.chaosdorf.meteroid.sync.SyncManager
 import de.chaosdorf.meteroid.util.update
@@ -97,6 +98,10 @@ class DrinkListViewModel @Inject constructor(
     }
   }
 
+  suspend fun checkOffline(server: Server?): Boolean =
+    if (server == null) true
+    else syncManager.checkOffline(server)
+
   enum class Filter {
     CaffeineFree,
     Active;
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
index 42d4837..d553e02 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt
@@ -32,7 +32,11 @@ import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
@@ -58,6 +62,18 @@ fun MoneyListScreen(
     }
   }
   val account by viewModel.account.collectAsState()
+  val snackbarHostState = remember { SnackbarHostState() }
+
+  LaunchedEffect(account) {
+    val offline = viewModel.checkOffline(account?.server)
+    snackbarHostState.currentSnackbarData?.dismiss()
+    if (offline) {
+      snackbarHostState.showSnackbar(
+        message = "Unable to connect to server",
+        duration = SnackbarDuration.Indefinite
+      )
+    }
+  }
 
   Scaffold(
     topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) },
@@ -67,6 +83,9 @@ fun MoneyListScreen(
         historyEnabled = account?.user?.audit == true,
         navigateTo = onNavigate
       )
+    },
+    snackbarHost = {
+      SnackbarHost(hostState = snackbarHostState)
     }
   ) { paddingValues: PaddingValues ->
     Column {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
index b568029..eb94fb6 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt
@@ -30,6 +30,7 @@ import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.R
 import de.chaosdorf.meteroid.model.AccountInfo
+import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.sync.AccountProvider
 import de.chaosdorf.meteroid.sync.SyncManager
 import kotlinx.coroutines.flow.SharingStarted
@@ -79,4 +80,8 @@ class MoneyListViewModel @Inject constructor(
       }
     }
   }
+
+  suspend fun checkOffline(server: Server?): Boolean =
+    if (server == null) true
+    else syncManager.checkOffline(server)
 }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
index 7950b83..26100e6 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt
@@ -31,8 +31,7 @@ import de.chaosdorf.mete.model.MeteApiFactory
 import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.ServerId
 import de.chaosdorf.meteroid.model.ServerRepository
-import de.chaosdorf.meteroid.util.findBestIcon
-import de.chaosdorf.meteroid.util.resolve
+import de.chaosdorf.meteroid.util.newServer
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -49,32 +48,15 @@ class AddServerViewModel @Inject constructor(
 ) : ViewModel() {
   val url = MutableStateFlow("")
 
-  private suspend fun buildServer(
-    id: ServerId,
-    url: String
-  ): Server? = try {
-    val api = factory.newInstance(url)
-    val manifest = api.getManifest()
-    val icon = manifest?.findBestIcon()
-    Server(
-      id,
-      manifest?.name,
-      url,
-      icon?.resolve(url)
-    )
-  } catch (_: Exception) {
-    null
-  }
-
   val server: StateFlow<Server?> = url
     .debounce(300.milliseconds)
-    .mapLatest { buildServer(ServerId(-1), it) }
+    .mapLatest { factory.newServer(ServerId(-1), it) }
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
 
   suspend fun addServer() {
     val highestServerId = repository.getAll().maxOfOrNull { it.serverId.value } ?: 0
     val serverId = ServerId(highestServerId + 1)
-    val server = buildServer(serverId, url.value)
+    val server = factory.newServer(serverId, url.value)
     if (server != null) {
       repository.save(server)
     }
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
index 73ecb0c..bc1eefc 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt
@@ -22,23 +22,24 @@
  * THE SOFTWARE.
  */
 
-package de.chaosdorf.meteroid.ui.Transactions
+package de.chaosdorf.meteroid.ui.transactions
 
 import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.ui.Modifier
+import androidx.compose.runtime.remember
 import androidx.navigation.NavOptions
 import de.chaosdorf.meteroid.ui.navigation.HomeSections
 import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
 import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
-import de.chaosdorf.meteroid.ui.transactions.TransactionListItem
-import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel
 
 @Composable
 fun TransactionListScreen(
@@ -47,6 +48,18 @@ fun TransactionListScreen(
 ) {
   val account by viewModel.account.collectAsState()
   val transactions by viewModel.transactions.collectAsState()
+  val snackbarHostState = remember { SnackbarHostState() }
+
+  LaunchedEffect(account) {
+    val offline = viewModel.checkOffline(account?.server)
+    snackbarHostState.currentSnackbarData?.dismiss()
+    if (offline) {
+      snackbarHostState.showSnackbar(
+        message = "Unable to connect to server",
+        duration = SnackbarDuration.Indefinite
+      )
+    }
+  }
 
   Scaffold(
     topBar = { MeteroidTopBar(account, onNavigate, viewModel::togglePin) },
@@ -56,6 +69,9 @@ fun TransactionListScreen(
         historyEnabled = account?.user?.audit == true,
         navigateTo = onNavigate
       )
+    },
+    snackbarHost = {
+      SnackbarHost(hostState = snackbarHostState)
     }
   ) { paddingValues: PaddingValues ->
     LazyColumn(contentPadding = paddingValues) {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
index 14a8853..15e41aa 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt
@@ -29,9 +29,11 @@ import androidx.lifecycle.viewModelScope
 import dagger.hilt.android.lifecycle.HiltViewModel
 import de.chaosdorf.meteroid.model.AccountInfo
 import de.chaosdorf.meteroid.model.DrinkRepository
+import de.chaosdorf.meteroid.model.Server
 import de.chaosdorf.meteroid.model.TransactionRepository
 import de.chaosdorf.meteroid.sync.AccountProvider
 import de.chaosdorf.meteroid.sync.SyncHandler
+import de.chaosdorf.meteroid.sync.SyncManager
 import de.chaosdorf.meteroid.sync.TransactionSyncHandler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -49,7 +51,8 @@ import kotlin.time.Duration.Companion.minutes
 class TransactionViewModel @Inject constructor(
   private val accountProvider: AccountProvider,
   repository: TransactionRepository,
-  drinkRepository: DrinkRepository
+  drinkRepository: DrinkRepository,
+  private val syncManager: SyncManager
 ) : ViewModel() {
   val account: StateFlow<AccountInfo?> = accountProvider.account
     .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@@ -85,6 +88,10 @@ class TransactionViewModel @Inject constructor(
       }
     }
   }
+
+  suspend fun checkOffline(server: Server?): Boolean =
+    if (server == null) true
+    else syncManager.checkOffline(server)
 }
 
 fun List<TransactionInfo>.mergeAdjecentDeposits(): List<TransactionInfo> {
diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt
similarity index 65%
rename from app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
rename to app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt
index 65416db..a9b65cf 100644
--- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/PwaManifestExtension.kt
+++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt
@@ -24,13 +24,30 @@
 
 package de.chaosdorf.meteroid.util
 
-import de.chaosdorf.mete.model.PwaIcon
-import de.chaosdorf.mete.model.PwaManifest
-import okhttp3.HttpUrl.Companion.toHttpUrl
+import de.chaosdorf.mete.model.MeteApiFactory
+import de.chaosdorf.meteroid.model.Server
+import de.chaosdorf.meteroid.model.ServerId
+import java.net.URI
 
-fun PwaManifest.findBestIcon(): PwaIcon? = icons.maxByOrNull {
-  it.sizes?.split("x")?.firstOrNull()?.toIntOrNull() ?: 0
-}
+suspend fun MeteApiFactory.newServer(serverId: ServerId, baseUrl: String): Server? = try {
+  val api = newInstance(baseUrl)
+  val manifest = api.getManifest()
+
+  val icon = manifest?.icons?.maxByOrNull {
+    it.sizes?.split("x")
+      ?.mapNotNull(String::toIntOrNull)
+      ?.reduce(Int::times)
+      ?: 0
+  }
 
-fun PwaIcon.resolve(baseUrl: String): String? =
-  this.src?.let { baseUrl.toHttpUrl().resolve(it) }?.toString()
+  val iconUrl = icon?.src?.let { URI(baseUrl).resolve(it).toString() }
+
+  Server(
+    serverId,
+    manifest?.name,
+    baseUrl,
+    iconUrl
+  )
+} catch (_: Exception) {
+  null
+}
-- 
GitLab