Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • main
1 result

Target

Select target project
  • justJanne/meteroid
1 result
Select Git revision
  • main
1 result
Show changes
Commits on Source (5)
Showing
with 510 additions and 237 deletions
...@@ -24,11 +24,19 @@ ...@@ -24,11 +24,19 @@
package de.chaosdorf.mete.model package de.chaosdorf.mete.model
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import retrofit2.http.Query
import java.math.BigDecimal import java.math.BigDecimal
interface MeteApi { interface MeteApi {
suspend fun getManifest(): PwaManifest? suspend fun getManifest(): PwaManifest?
suspend fun listTransactions(user: UserId? = null): TransactionSummaryModel suspend fun listTransactions(
user: UserId,
startYear: Int, startMonth: Int, startDay: Int,
endYear: Int, endMonth: Int, endDay: Int,
): TransactionSummaryModel
suspend fun listBarcodes(): List<BarcodeModel> suspend fun listBarcodes(): List<BarcodeModel>
suspend fun getBarcode(id: BarcodeId): BarcodeModel? suspend fun getBarcode(id: BarcodeId): BarcodeModel?
suspend fun listDrinks(): List<DrinkModel> suspend fun listDrinks(): List<DrinkModel>
......
...@@ -28,12 +28,16 @@ import de.chaosdorf.mete.model.BarcodeId ...@@ -28,12 +28,16 @@ import de.chaosdorf.mete.model.BarcodeId
import de.chaosdorf.mete.model.DrinkId import de.chaosdorf.mete.model.DrinkId
import de.chaosdorf.mete.model.MeteApi import de.chaosdorf.mete.model.MeteApi
import de.chaosdorf.mete.model.PwaManifest import de.chaosdorf.mete.model.PwaManifest
import de.chaosdorf.mete.model.TransactionSummaryModel
import de.chaosdorf.mete.model.UserId import de.chaosdorf.mete.model.UserId
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import retrofit2.Response import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
import java.math.BigDecimal import java.math.BigDecimal
import java.time.Year
internal interface MeteApiV1 : MeteApi { internal interface MeteApiV1 : MeteApi {
@GET("manifest.json") @GET("manifest.json")
...@@ -41,7 +45,13 @@ internal interface MeteApiV1 : MeteApi { ...@@ -41,7 +45,13 @@ internal interface MeteApiV1 : MeteApi {
@GET("api/v1/audits.json") @GET("api/v1/audits.json")
override suspend fun listTransactions( override suspend fun listTransactions(
@Query("user") user: UserId? @Query("user") user: UserId,
@Query("start_date[year]") startYear: Int,
@Query("start_date[month]") startMonth: Int,
@Query("start_date[day]") startDay: Int,
@Query("end_date[year]") endYear: Int,
@Query("end_date[month]") endMonth: Int,
@Query("end_date[day]") endDay: Int,
): TransactionSummaryModelV1 ): TransactionSummaryModelV1
@GET("api/v1/barcodes.json") @GET("api/v1/barcodes.json")
......
...@@ -73,6 +73,8 @@ dependencies { ...@@ -73,6 +73,8 @@ dependencies {
implementation(libs.androidx.activity) implementation(libs.androidx.activity)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.splashscreen)
implementation(libs.androidx.compose.animation) implementation(libs.androidx.compose.animation)
implementation(libs.androidx.compose.compiler) implementation(libs.androidx.compose.compiler)
implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.foundation)
......
...@@ -9,12 +9,11 @@ ...@@ -9,12 +9,11 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/application_name" android:label="@string/application_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Meteroid"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.Meteroid"> android:theme="@style/Theme.Meteroid.SplashScreen">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
......
...@@ -27,18 +27,29 @@ package de.chaosdorf.meteroid ...@@ -27,18 +27,29 @@ package de.chaosdorf.meteroid
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.get
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import de.chaosdorf.meteroid.ui.AppRouter import de.chaosdorf.meteroid.ui.AppRouter
import de.chaosdorf.meteroid.ui.AppViewModel
import de.chaosdorf.meteroid.ui.theme.MeteroidTheme import de.chaosdorf.meteroid.ui.theme.MeteroidTheme
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val viewModelProvider = ViewModelProvider(this)
val viewModel = viewModelProvider.get<AppViewModel>()
installSplashScreen().setKeepOnScreenCondition {
viewModel.initialBackStack.value == null
}
setContent { setContent {
MeteroidTheme { MeteroidTheme {
AppRouter() AppRouter(viewModel)
} }
} }
} }
......
...@@ -37,5 +37,5 @@ interface AccountPreferences { ...@@ -37,5 +37,5 @@ interface AccountPreferences {
val state: Flow<State> val state: Flow<State>
suspend fun setServer(server: ServerId?) suspend fun setServer(server: ServerId?)
suspend fun setUser(user: UserId?) suspend fun setUser(server: ServerId, user: UserId?)
} }
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
package de.chaosdorf.meteroid.storage package de.chaosdorf.meteroid.storage
import android.util.Log
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
...@@ -42,6 +43,7 @@ class AccountPreferencesImpl @Inject constructor( ...@@ -42,6 +43,7 @@ class AccountPreferencesImpl @Inject constructor(
dataStore.data.mapLatest { dataStore.data.mapLatest {
val serverId = it[SERVER_KEY] ?: -1L val serverId = it[SERVER_KEY] ?: -1L
val userId = it[USER_KEY] ?: -1L val userId = it[USER_KEY] ?: -1L
Log.i("AccountPreferences", "Restored account data: $userId@$serverId")
AccountPreferences.State( AccountPreferences.State(
if (serverId >= 0) ServerId(serverId) else null, if (serverId >= 0) ServerId(serverId) else null,
...@@ -50,13 +52,17 @@ class AccountPreferencesImpl @Inject constructor( ...@@ -50,13 +52,17 @@ class AccountPreferencesImpl @Inject constructor(
} }
override suspend fun setServer(server: ServerId?) { override suspend fun setServer(server: ServerId?) {
Log.i("AccountPreferences", "Setting account to -1@${server?.value ?: -1L}")
dataStore.edit { dataStore.edit {
it[SERVER_KEY] = server?.value ?: -1L it[SERVER_KEY] = server?.value ?: -1L
it[USER_KEY] = -1L
} }
} }
override suspend fun setUser(user: UserId?) { override suspend fun setUser(server: ServerId, user: UserId?) {
Log.i("AccountPreferences", "Setting account to ${user?.value ?: -1L}@${server.value}")
dataStore.edit { dataStore.edit {
it[SERVER_KEY] = server.value
it[USER_KEY] = user?.value ?: -1L it[USER_KEY] = user?.value ?: -1L
} }
} }
......
...@@ -31,39 +31,21 @@ import de.chaosdorf.meteroid.model.PinnedUserRepository ...@@ -31,39 +31,21 @@ import de.chaosdorf.meteroid.model.PinnedUserRepository
import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.UserRepository import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.storage.AccountPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject import javax.inject.Inject
class AccountProvider @Inject constructor( class AccountProvider @Inject constructor(
accountPreferences: AccountPreferences, private val serverRepository: ServerRepository,
serverRepository: ServerRepository, private val userRepository: UserRepository,
userRepository: UserRepository,
private val pinnedUserRepository: PinnedUserRepository, private val pinnedUserRepository: PinnedUserRepository,
) { ) {
val account: Flow<AccountInfo?> = fun accountFlow(serverId: ServerId, userId: UserId) = combine(
accountPreferences.state.flatMapLatest { preferences -> serverRepository.getFlow(serverId),
if (preferences.server == null) { userRepository.getFlow(serverId, userId),
flowOf(null) pinnedUserRepository.isPinnedFlow(serverId, userId)
} else { ) { server, user, pinned ->
serverRepository.getFlow(preferences.server).flatMapLatest { server -> if (server == null || user == null) null
if (server == null) { else AccountInfo(server, user, pinned)
flowOf(null)
} else if (preferences.user == null) {
flowOf(AccountInfo(server, null, false))
} else {
combine(
userRepository.getFlow(server.serverId, preferences.user),
pinnedUserRepository.isPinnedFlow(server.serverId, preferences.user)
) { user, pinned ->
AccountInfo(server, user, pinned)
}
}
}
}
} }
suspend fun togglePin(serverId: ServerId, userId: UserId) { suspend fun togglePin(serverId: ServerId, userId: UserId) {
......
...@@ -32,18 +32,19 @@ import de.chaosdorf.meteroid.model.Drink ...@@ -32,18 +32,19 @@ import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.model.Server import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.sync.base.BaseSyncHandler
import javax.inject.Inject import javax.inject.Inject
class DrinkSyncHandler @Inject constructor( class DrinkSyncHandler @Inject constructor(
private val factory: MeteApiFactory, private val factory: MeteApiFactory,
private val db: MeteroidDatabase, private val db: MeteroidDatabase,
private val repository: DrinkRepository private val repository: DrinkRepository
) : SyncHandler<Server, Drink, DrinkSyncHandler.Key>() { ) : BaseSyncHandler<Server, Drink, DrinkSyncHandler.Key>() {
data class Key( data class Key(
val server: ServerId, val drink: DrinkId val server: ServerId, val drink: DrinkId
) )
override suspend fun withTransaction(block: suspend () -> Unit) = override suspend fun <T>withTransaction(block: suspend () -> T): T =
db.withTransaction(block) db.withTransaction(block)
override suspend fun store(entry: Drink) = override suspend fun store(entry: Drink) =
......
...@@ -32,8 +32,6 @@ import de.chaosdorf.meteroid.model.Server ...@@ -32,8 +32,6 @@ import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.util.newServer import de.chaosdorf.meteroid.util.newServer
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.math.BigDecimal import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
...@@ -54,13 +52,17 @@ class SyncManager @Inject constructor( ...@@ -54,13 +52,17 @@ class SyncManager @Inject constructor(
} }
} }
suspend fun sync(server: Server, user: User?) { suspend fun sync(server: Server, user: User?, incremental: Boolean) {
try { try {
userSyncHandler.sync(server) userSyncHandler.sync(server)
drinkSyncHandler.sync(server) drinkSyncHandler.sync(server)
if (user != null) { if (user != null) {
if (incremental) {
transactionSyncHandler.syncIncremental(Pair(server, user.userId))
} else {
transactionSyncHandler.sync(Pair(server, user.userId)) transactionSyncHandler.sync(Pair(server, user.userId))
} }
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e( Log.e(
"Sync", "Sync",
...@@ -71,50 +73,44 @@ class SyncManager @Inject constructor( ...@@ -71,50 +73,44 @@ class SyncManager @Inject constructor(
} }
suspend fun purchase(account: AccountInfo, drink: Drink) { suspend fun purchase(account: AccountInfo, drink: Drink) {
account.user?.let { user ->
val api = factory.newInstance(account.server.url) val api = factory.newInstance(account.server.url)
try { try {
api.purchase(user.userId, drink.drinkId) api.purchase(account.user.userId, drink.drinkId)
sync(account.server, user) sync(account.server, account.user, incremental = true)
} catch (e: Exception) { } catch (e: Exception) {
Log.e( Log.e(
"Sync", "Sync",
"Could not finish transaction for ${user.name} (${user.userId}) on ${account.server.url}", "Could not finish transaction for ${account.user.name} (${account.user.userId}) on ${account.server.url}",
e e
) )
} }
} }
}
suspend fun deposit(account: AccountInfo, amount: BigDecimal) { suspend fun deposit(account: AccountInfo, amount: BigDecimal) {
account.user?.let { user ->
try { try {
val api = factory.newInstance(account.server.url) val api = factory.newInstance(account.server.url)
api.deposit(user.userId, amount) api.deposit(account.user.userId, amount)
sync(account.server, user) sync(account.server, account.user, incremental = true)
} catch (e: Exception) { } catch (e: Exception) {
Log.e( Log.e(
"Sync", "Sync",
"Could not finish transaction for ${user.name} (${user.userId}) on ${account.server.url}", "Could not finish transaction for ${account.user.name} (${account.user.userId}) on ${account.server.url}",
e e
) )
} }
} }
}
suspend fun withdraw(account: AccountInfo, amount: BigDecimal) { suspend fun withdraw(account: AccountInfo, amount: BigDecimal) {
account.user?.let { user ->
val api = factory.newInstance(account.server.url) val api = factory.newInstance(account.server.url)
try { try {
api.withdraw(user.userId, amount) api.withdraw(account.user.userId, amount)
sync(account.server, user) sync(account.server, account.user, incremental = true)
} catch (e: Exception) { } catch (e: Exception) {
Log.e( Log.e(
"Sync", "Sync",
"Could not finish transaction for ${user.name} (${user.userId}) on ${account.server.url}", "Could not finish transaction for ${account.user.name} (${account.user.userId}) on ${account.server.url}",
e e
) )
} }
} }
} }
}
...@@ -33,18 +33,25 @@ import de.chaosdorf.meteroid.model.Server ...@@ -33,18 +33,25 @@ import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.Transaction import de.chaosdorf.meteroid.model.Transaction
import de.chaosdorf.meteroid.model.TransactionRepository import de.chaosdorf.meteroid.model.TransactionRepository
import de.chaosdorf.meteroid.sync.base.BaseIncrementalSyncHandler
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.plus
import kotlinx.datetime.toLocalDateTime
import javax.inject.Inject import javax.inject.Inject
class TransactionSyncHandler @Inject constructor( class TransactionSyncHandler @Inject constructor(
private val factory: MeteApiFactory, private val factory: MeteApiFactory,
private val db: MeteroidDatabase, private val db: MeteroidDatabase,
private val repository: TransactionRepository private val repository: TransactionRepository
) : SyncHandler<Pair<Server, UserId>, Transaction, TransactionSyncHandler.Key>() { ) : BaseIncrementalSyncHandler<Pair<Server, UserId>, Transaction, TransactionSyncHandler.Key>() {
data class Key( data class Key(
val server: ServerId, val transaction: TransactionId val server: ServerId, val transaction: TransactionId
) )
override suspend fun withTransaction(block: suspend () -> Unit) = override suspend fun <T> withTransaction(block: suspend () -> T): T =
db.withTransaction(block) db.withTransaction(block)
override suspend fun store(entry: Transaction) = override suspend fun store(entry: Transaction) =
...@@ -61,7 +68,36 @@ class TransactionSyncHandler @Inject constructor( ...@@ -61,7 +68,36 @@ class TransactionSyncHandler @Inject constructor(
override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Transaction> { override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Transaction> {
val (server, userId) = context val (server, userId) = context
val api = factory.newInstance(server.url) val api = factory.newInstance(server.url)
val loadedEntries = api.listTransactions(user = userId).entries val startDate = LocalDate(1970, 1, 1)
val endDate = Clock.System.now().toLocalDateTime(TimeZone.UTC).date
.plus(1, DateTimeUnit.DAY)
val loadedEntries = api.listTransactions(
userId,
startDate.year, startDate.month.value, startDate.dayOfMonth,
endDate.year, endDate.month.value, endDate.dayOfMonth,
).entries
return loadedEntries.map { Transaction.fromModel(server, userId, it) }
}
override suspend fun loadLatestEntry(context: Pair<Server, UserId>): Transaction? =
repository.getLatest(context.first.serverId, context.second)
override suspend fun loadAdded(
context: Pair<Server, UserId>,
latest: Transaction
): List<Transaction> {
val (server, userId) = context
val api = factory.newInstance(server.url)
val startDate = latest.timestamp.toLocalDateTime(TimeZone.UTC).date
val endDate = Clock.System.now().toLocalDateTime(TimeZone.UTC).date
.plus(1, DateTimeUnit.DAY)
val loadedEntries = api.listTransactions(
userId,
startDate.year, startDate.month.value, startDate.dayOfMonth,
endDate.year, endDate.month.value, endDate.dayOfMonth,
).entries
return loadedEntries.map { Transaction.fromModel(server, userId, it) } return loadedEntries.map { Transaction.fromModel(server, userId, it) }
} }
} }
...@@ -32,18 +32,19 @@ import de.chaosdorf.meteroid.model.Server ...@@ -32,18 +32,19 @@ import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.model.UserRepository import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.sync.base.BaseSyncHandler
import javax.inject.Inject import javax.inject.Inject
class UserSyncHandler @Inject constructor( class UserSyncHandler @Inject constructor(
private val factory: MeteApiFactory, private val factory: MeteApiFactory,
private val db: MeteroidDatabase, private val db: MeteroidDatabase,
private val repository: UserRepository private val repository: UserRepository
) : SyncHandler<Server, User, UserSyncHandler.Key>() { ) : BaseSyncHandler<Server, User, UserSyncHandler.Key>() {
data class Key( data class Key(
val server: ServerId, val user: UserId val server: ServerId, val user: UserId
) )
override suspend fun withTransaction(block: suspend () -> Unit) = override suspend fun <T>withTransaction(block: suspend () -> T): T =
db.withTransaction(block) db.withTransaction(block)
override suspend fun store(entry: User) = override suspend fun store(entry: User) =
......
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.sync.base
import android.util.Log
abstract class BaseIncrementalSyncHandler<Context, Entry, Key> :
BaseSyncHandler<Context, Entry, Key>(),
IncrementalSyncHandler<Context> {
abstract suspend fun loadLatestEntry(context: Context): Entry?
abstract suspend fun loadAdded(context: Context, latest: Entry): List<Entry>
override suspend fun syncIncremental(context: Context) {
if (syncState.compareAndSet(State.Idle, State.Loading) ||
syncState.compareAndSet(State.Error(), State.Loading)) {
Log.w(this::class.simpleName, "Started incremental sync")
val success = try {
val result = withTransaction {
val latestEntry = loadLatestEntry(context)
if (latestEntry != null) {
val addedEntries = loadAdded(context, latestEntry)
for (loadedEntry in addedEntries) {
store(loadedEntry)
}
true
} else {
false
}
}
syncState.value = State.Idle
Log.w(this::class.simpleName, "Finished incremental sync")
result
} catch (e: Exception) {
Log.e(this::class.simpleName, "Error while syncing data", e)
syncState.value = State.Error("Error while syncing data: $e")
false
}
// If we can't do an incremental sync, do a full sync
if (!success) {
sync(context)
}
} else {
Log.w(this::class.simpleName, "Already syncing, disregarding sync request")
}
}
}
...@@ -22,13 +22,13 @@ ...@@ -22,13 +22,13 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.sync package de.chaosdorf.meteroid.sync.base
import android.util.Log import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
abstract class SyncHandler<Context, Entry, Key> { abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> {
sealed class State { sealed class State {
data object Idle : State() data object Idle : State()
data object Loading : State() data object Loading : State()
...@@ -45,7 +45,7 @@ abstract class SyncHandler<Context, Entry, Key> { ...@@ -45,7 +45,7 @@ abstract class SyncHandler<Context, Entry, Key> {
} }
} }
abstract suspend fun withTransaction(block: suspend () -> Unit) abstract suspend fun <T>withTransaction(block: suspend () -> T): T
abstract suspend fun loadCurrent(context: Context): List<Entry> abstract suspend fun loadCurrent(context: Context): List<Entry>
abstract suspend fun loadStored(context: Context): List<Entry> abstract suspend fun loadStored(context: Context): List<Entry>
...@@ -55,15 +55,12 @@ abstract class SyncHandler<Context, Entry, Key> { ...@@ -55,15 +55,12 @@ abstract class SyncHandler<Context, Entry, Key> {
abstract suspend fun delete(key: Key) abstract suspend fun delete(key: Key)
abstract suspend fun store(entry: Entry) abstract suspend fun store(entry: Entry)
private val _state = MutableStateFlow<State>(State.Idle) protected val syncState = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> = _state val state: StateFlow<State> = syncState
suspend fun sync(context: Context) { override suspend fun sync(context: Context) {
if (_state.compareAndSet(State.Idle, State.Loading) || _state.compareAndSet( if (syncState.compareAndSet(State.Idle, State.Loading) ||
State.Error(), syncState.compareAndSet(State.Error(), State.Loading)) {
State.Loading
)
) {
Log.w(this::class.simpleName, "Started sync") Log.w(this::class.simpleName, "Started sync")
try { try {
val loadedEntries = loadCurrent(context) val loadedEntries = loadCurrent(context)
...@@ -80,11 +77,11 @@ abstract class SyncHandler<Context, Entry, Key> { ...@@ -80,11 +77,11 @@ abstract class SyncHandler<Context, Entry, Key> {
store(loadedEntry) store(loadedEntry)
} }
} }
_state.value = State.Idle syncState.value = State.Idle
Log.w(this::class.simpleName, "Finished sync") Log.w(this::class.simpleName, "Finished sync")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(this::class.simpleName, "Error while syncing data", e) Log.e(this::class.simpleName, "Error while syncing data", e)
_state.value = State.Error("Error while syncing data: $e") syncState.value = State.Error("Error while syncing data: $e")
} }
} else { } else {
Log.w(this::class.simpleName, "Already syncing, disregarding sync request") Log.w(this::class.simpleName, "Already syncing, disregarding sync request")
......
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.sync.base
interface IncrementalSyncHandler<Context> {
suspend fun syncIncremental(context: Context)
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.sync.base
interface SyncHandler<Context> {
suspend fun sync(context: Context)
}
...@@ -24,119 +24,110 @@ ...@@ -24,119 +24,110 @@
package de.chaosdorf.meteroid.ui package de.chaosdorf.meteroid.ui
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.material3.Scaffold
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavType
import androidx.navigation.NavOptions
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen
import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel
import de.chaosdorf.meteroid.ui.money.MoneyListScreen import de.chaosdorf.meteroid.ui.money.MoneyListScreen
import de.chaosdorf.meteroid.ui.money.MoneyListViewModel import de.chaosdorf.meteroid.ui.money.MoneyListViewModel
import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
import de.chaosdorf.meteroid.ui.navigation.Routes import de.chaosdorf.meteroid.ui.navigation.Routes
import de.chaosdorf.meteroid.ui.servers.AddServerScreen import de.chaosdorf.meteroid.ui.servers.AddServerScreen
import de.chaosdorf.meteroid.ui.servers.AddServerViewModel
import de.chaosdorf.meteroid.ui.servers.ServerListScreen import de.chaosdorf.meteroid.ui.servers.ServerListScreen
import de.chaosdorf.meteroid.ui.servers.ServerListViewModel
import de.chaosdorf.meteroid.ui.transactions.TransactionListScreen import de.chaosdorf.meteroid.ui.transactions.TransactionListScreen
import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel
import de.chaosdorf.meteroid.ui.users.UserListScreen import de.chaosdorf.meteroid.ui.users.UserListScreen
import de.chaosdorf.meteroid.ui.users.UserListViewModel
import de.chaosdorf.meteroid.ui.wrapped.WrappedScreen import de.chaosdorf.meteroid.ui.wrapped.WrappedScreen
import de.chaosdorf.meteroid.ui.wrapped.WrappedViewModel import de.chaosdorf.meteroid.ui.wrapped.WrappedViewModel
import kotlinx.coroutines.launch import de.chaosdorf.meteroid.util.popUpToRoot
@Composable @Composable
fun AppRouter(viewModel: AppViewModel = viewModel()) { fun AppRouter(viewModel: AppViewModel) {
val scope = rememberCoroutineScope()
val navController = rememberNavController() val navController = rememberNavController()
val initState by viewModel.initState.collectAsState() val snackbarHostState = remember { SnackbarHostState() }
val navigationViewModel = hiltViewModel<NavigationViewModel>()
LaunchedEffect(initState) { val initialBackStack by viewModel.initialBackStack.collectAsState()
when (initState) { val offline by viewModel.offline.collectAsState()
AppViewModel.InitState.LOADING -> navController.navigate(
Routes.Init,
NavOptions.Builder().setPopUpTo(Routes.Init, true).build()
)
AppViewModel.InitState.CREATE_SERVER -> navController.navigate( navController.addOnDestinationChangedListener { _, _, arguments ->
Routes.Servers.Add, val serverId = arguments?.getLong("server")?.let(::ServerId)
NavOptions.Builder().setPopUpTo(Routes.Servers.Add, true).build() val userId = arguments?.getLong("user")?.let(::UserId)
)
AppViewModel.InitState.SELECT_SERVER -> navController.navigate( navigationViewModel.serverId.value = serverId
Routes.Servers.List, navigationViewModel.userId.value = userId
NavOptions.Builder().setPopUpTo(Routes.Servers.List, true).build()
)
AppViewModel.InitState.SELECT_USER -> navController.navigate(
Routes.Users.List,
NavOptions.Builder().setPopUpTo(Routes.Users.List, true).build()
)
AppViewModel.InitState.HOME -> navController.navigate(
Routes.Home.Root,
NavOptions.Builder().setPopUpTo(Routes.Home.Root, true).build()
)
}
} }
NavHost(navController, startDestination = Routes.Init) { LaunchedEffect(initialBackStack) {
composable(route = Routes.Init) { _ -> initialBackStack?.let { initialBackStack ->
Box(Modifier.fillMaxSize()) { navController.popUpToRoot()
Column(Modifier.align(Alignment.Center)) { for (entry in initialBackStack) {
CircularProgressIndicator() navController.navigate(entry)
Text("Loading")
} }
} }
} }
navigation(route = Routes.Servers.Root, startDestination = Routes.Servers.List) { LaunchedEffect(offline) {
composable(Routes.Servers.List) { _ -> snackbarHostState.currentSnackbarData?.dismiss()
ServerListScreen( if (offline) {
hiltViewModel(), snackbarHostState.showSnackbar(
onAdd = { navController.navigate(Routes.Servers.Add) }, message = "Unable to connect to server",
onSelect = { duration = SnackbarDuration.Indefinite
scope.launch {
viewModel.selectServer(it)
navController.navigate(Routes.Users.List)
}
}
)
}
composable(Routes.Servers.Add) { _ ->
AddServerScreen(
hiltViewModel(),
isFirst = initState == AppViewModel.InitState.CREATE_SERVER,
onBack = { navController.navigate(Routes.Servers.List) },
onAdd = { navController.navigate(Routes.Servers.List) }
) )
} }
} }
navigation(route = Routes.Users.Root, startDestination = Routes.Users.List) {
composable(Routes.Users.List) { _ -> Box {
UserListScreen( Scaffold(
hiltViewModel(), topBar = {
onAdd = { TODO() }, MeteroidTopBar(navController, navigationViewModel, Modifier.align(Alignment.TopCenter))
onSelect = {
scope.launch {
viewModel.selectUser(it)
navController.navigate(Routes.Home.Root)
}
}, },
onBack = { navController.navigate(Routes.Servers.Root) }, bottomBar = {
) MeteroidBottomBar(navController, navigationViewModel)
},
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { paddingValues ->
NavHost(
navController,
startDestination = Routes.Servers.List,
enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None },
popEnterTransition = { EnterTransition.None },
popExitTransition = { ExitTransition.None }
) {
composable(Routes.Servers.Add) { _ ->
val hiltViewModel = hiltViewModel<AddServerViewModel>()
AddServerScreen(navController, hiltViewModel, paddingValues)
}
composable(Routes.Servers.List) { _ ->
val hiltViewModel = hiltViewModel<ServerListViewModel>()
ServerListScreen(navController, hiltViewModel, paddingValues)
} }
/* /*
composable(Routes.Users.Add) { _ -> composable(Routes.Users.Add) { _ ->
...@@ -146,23 +137,55 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) { ...@@ -146,23 +137,55 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) {
) )
} }
*/ */
composable(
Routes.Users.List,
arguments = listOf(
navArgument("server") { type = NavType.LongType }
)
) { _ ->
val hiltViewModel = hiltViewModel<UserListViewModel>()
UserListScreen(navController, hiltViewModel, paddingValues)
}
composable(
Routes.Home.Purchase,
arguments = listOf(
navArgument("server") { type = NavType.LongType },
navArgument("user") { type = NavType.LongType },
)
) { _ ->
val hiltViewModel = hiltViewModel<DrinkListViewModel>()
DrinkListScreen(navController, hiltViewModel, paddingValues)
}
composable(
Routes.Home.Deposit,
arguments = listOf(
navArgument("server") { type = NavType.LongType },
navArgument("user") { type = NavType.LongType },
)
) { _ ->
val hiltViewModel = hiltViewModel<MoneyListViewModel>()
MoneyListScreen(navController, hiltViewModel, paddingValues)
}
composable(
Routes.Home.History,
arguments = listOf(
navArgument("server") { type = NavType.LongType },
navArgument("user") { type = NavType.LongType },
)
) { _ ->
val hiltViewModel = hiltViewModel<TransactionViewModel>()
TransactionListScreen(hiltViewModel, paddingValues)
}
composable(
Routes.Home.Wrapped,
arguments = listOf(
navArgument("server") { type = NavType.LongType },
navArgument("user") { type = NavType.LongType },
)
) { _ ->
val hiltViewModel = hiltViewModel<WrappedViewModel>()
WrappedScreen(hiltViewModel, paddingValues)
} }
navigation(route = Routes.Home.Root, startDestination = Routes.Home.Purchase) {
composable(Routes.Home.Purchase) { _ ->
val drinkListViewModel = hiltViewModel<DrinkListViewModel>()
DrinkListScreen(drinkListViewModel, navController::navigate)
}
composable(Routes.Home.Deposit) { _ ->
val moneyListViewModel = hiltViewModel<MoneyListViewModel>()
MoneyListScreen(moneyListViewModel, navController::navigate)
}
composable(Routes.Home.History) { _ ->
val transactionViewModel = hiltViewModel<TransactionViewModel>()
TransactionListScreen(transactionViewModel, navController::navigate)
}
composable(Routes.Home.Wrapped) { _ ->
val wrappedViewModel = hiltViewModel<WrappedViewModel>()
WrappedScreen(wrappedViewModel, navController::navigate)
} }
} }
} }
......
...@@ -27,64 +27,58 @@ package de.chaosdorf.meteroid.ui ...@@ -27,64 +27,58 @@ package de.chaosdorf.meteroid.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.storage.AccountPreferences import de.chaosdorf.meteroid.storage.AccountPreferences
import de.chaosdorf.meteroid.sync.AccountProvider
import de.chaosdorf.meteroid.sync.SyncManager import de.chaosdorf.meteroid.sync.SyncManager
import de.chaosdorf.meteroid.ui.navigation.Routes
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AppViewModel @Inject constructor( class AppViewModel @Inject constructor(
accountProvider: AccountProvider, accountPreferences: AccountPreferences,
private val accountPreferences: AccountPreferences,
private val serverRepository: ServerRepository, private val serverRepository: ServerRepository,
private val userRepository: UserRepository,
private val syncManager: SyncManager private val syncManager: SyncManager
) : ViewModel() { ) : ViewModel() {
val initState: StateFlow<InitState> = accountPreferences.state val initialBackStack = accountPreferences.state.flatMapLatest {
.flatMapLatest { preferences -> combine(
serverRepository.getAllFlow() serverRepository.countFlow(),
.mapLatest { it.map(Server::serverId) } if (it.server == null) flowOf(null)
.mapLatest { servers -> else serverRepository.getFlow(it.server),
if (servers.isEmpty()) InitState.CREATE_SERVER if (it.server == null || it.user == null) flowOf(null)
else if (!servers.contains(preferences.server)) InitState.SELECT_SERVER else userRepository.getFlow(it.server, it.user)
else if (preferences.user == null) InitState.SELECT_USER ) { serverCount, server, user ->
else InitState.HOME if (user != null) {
listOf(
Routes.Servers.List,
Routes.Users.list(user.serverId),
Routes.Home.purchase(user.serverId, user.userId)
)
} else if (server != null) {
listOf(
Routes.Servers.List,
Routes.Users.list(server.serverId)
)
} else if (serverCount > 0) {
listOf(Routes.Servers.List)
} else {
listOf(Routes.Servers.Add)
} }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), InitState.LOADING)
init {
accountProvider.account.distinctUntilChanged().onEach { account ->
account?.let { (server, maybeUser) ->
syncManager.sync(server, maybeUser)
}
}.launchIn(viewModelScope)
}
suspend fun selectServer(server: ServerId) {
accountPreferences.setServer(server)
accountPreferences.setUser(null)
} }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
suspend fun selectUser(user: UserId) { val offline = accountPreferences.state.flatMapLatest { state ->
accountPreferences.setUser(user) state.server?.let { serverId ->
} serverRepository.getFlow(serverId)
} ?: flowOf(null)
enum class InitState { }.mapLatest { server ->
LOADING, server?.let { syncManager.checkOffline(it) } ?: false
CREATE_SERVER, }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false)
SELECT_SERVER,
SELECT_USER,
HOME
}
} }
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.PinnedUserRepository
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.sync.AccountProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class NavigationViewModel @Inject constructor(
serverRepository: ServerRepository,
userRepository: UserRepository,
pinnedUserRepository: PinnedUserRepository,
private val accountProvider: AccountProvider
) : ViewModel() {
val serverId = MutableStateFlow<ServerId?>(null)
val userId = MutableStateFlow<UserId?>(null)
val server = serverId.flatMapLatest {
if (it == null) flowOf(null)
else serverRepository.getFlow(it)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val user = combine(serverId, userId) { serverId, userId ->
if (serverId == null || userId == null) null
else Pair(serverId, userId)
}.flatMapLatest {
if (it == null) flowOf(null)
else userRepository.getFlow(it.first, it.second)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val pinned = combine(serverId, userId) { serverId, userId ->
if (serverId == null || userId == null) null
else Pair(serverId, userId)
}.flatMapLatest {
if (it == null) flowOf(null)
else pinnedUserRepository.isPinnedFlow(it.first, it.second)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun togglePin(serverId: ServerId, userId: UserId) {
viewModelScope.launch {
accountProvider.togglePin(serverId, userId)
}
}
}
...@@ -41,10 +41,10 @@ fun PriceBadge( ...@@ -41,10 +41,10 @@ fun PriceBadge(
price: BigDecimal, price: BigDecimal,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
containerColor: Color = containerColor: Color =
if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.primary if (price >= BigDecimal.ZERO) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.error, else MaterialTheme.colorScheme.error,
textColor: Color = textColor: Color =
if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary if (price >= BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onError, else MaterialTheme.colorScheme.onError,
textStyle: TextStyle = MaterialTheme.typography.labelLarge textStyle: TextStyle = MaterialTheme.typography.labelLarge
) { ) {
......