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 bb82525826cfdfb6e2c911fde6d9444357ec4e61..c217089b28f93c7bce8eea31d3a64bd1963f5f2f 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 f7237af875345c6e1c6bb1e58c2d3e5207e6e2cc..a43e4cacfbe397ee8d31dbbb00124f66e9fa41eb 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 b69910067205b42baad8237a68506e47ad05a325..6604ee78fe352e078c380428d6f8cd801d340fad 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 162323de9ad50bf1c953685aa13fe66a21eb9952..e56a782ba0ca524025f9aadd6a574900592ee836 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 42d4837b4001f1988f1d62b04fae51da67365397..d553e02dfc60f6fd7dc0b8983647fb9dfe73c956 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 b568029242142dda2ad81ff71133ea290ab16c2f..eb94fb658b9382521196e769c439b7eb52c37aca 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 7950b83ed59f9bed43d443a29b2801bc66ba6da2..26100e67525c906f7b864242eaa37678bf7a2f0c 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 73ecb0ce9a7c36ca9226b4174b5db88e00493b52..bc1eefc70d93302aca1e1e7942d61f382fca37ee 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 14a88535bcabeb3d6077289754cca962ae539b57..15e41aa85b282727d8332841ee72e565b6f65096 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 65416dbf694bbcba2b3dfae7e1c48af95aa2d147..a9b65cfa0cfec0d03e965d35c78c7398b3d7a32c 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 +}