Skip to content
Snippets Groups Projects
Verified Commit 0f27a74c authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

feat: support incremental syncs

parent 41d35e71
No related branches found
No related tags found
No related merge requests found
Showing
with 283 additions and 75 deletions
......@@ -24,11 +24,19 @@
package de.chaosdorf.mete.model
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import retrofit2.http.Query
import java.math.BigDecimal
interface MeteApi {
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 getBarcode(id: BarcodeId): BarcodeModel?
suspend fun listDrinks(): List<DrinkModel>
......
......@@ -28,12 +28,16 @@ import de.chaosdorf.mete.model.BarcodeId
import de.chaosdorf.mete.model.DrinkId
import de.chaosdorf.mete.model.MeteApi
import de.chaosdorf.mete.model.PwaManifest
import de.chaosdorf.mete.model.TransactionSummaryModel
import de.chaosdorf.mete.model.UserId
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import java.math.BigDecimal
import java.time.Year
internal interface MeteApiV1 : MeteApi {
@GET("manifest.json")
......@@ -41,7 +45,13 @@ internal interface MeteApiV1 : MeteApi {
@GET("api/v1/audits.json")
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
@GET("api/v1/barcodes.json")
......
......@@ -32,18 +32,19 @@ import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.sync.base.BaseSyncHandler
import javax.inject.Inject
class DrinkSyncHandler @Inject constructor(
private val factory: MeteApiFactory,
private val db: MeteroidDatabase,
private val repository: DrinkRepository
) : SyncHandler<Server, Drink, DrinkSyncHandler.Key>() {
) : BaseSyncHandler<Server, Drink, DrinkSyncHandler.Key>() {
data class Key(
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)
override suspend fun store(entry: Drink) =
......
......@@ -54,13 +54,17 @@ class SyncManager @Inject constructor(
}
}
suspend fun sync(server: Server, user: User?) {
suspend fun sync(server: Server, user: User?, incremental: Boolean) {
try {
userSyncHandler.sync(server)
drinkSyncHandler.sync(server)
if (user != null) {
if (incremental) {
transactionSyncHandler.syncIncremental(Pair(server, user.userId))
} else {
transactionSyncHandler.sync(Pair(server, user.userId))
}
}
} catch (e: Exception) {
Log.e(
"Sync",
......@@ -75,7 +79,7 @@ class SyncManager @Inject constructor(
val api = factory.newInstance(account.server.url)
try {
api.purchase(user.userId, drink.drinkId)
sync(account.server, user)
sync(account.server, user, incremental = true)
} catch (e: Exception) {
Log.e(
"Sync",
......@@ -91,7 +95,7 @@ class SyncManager @Inject constructor(
try {
val api = factory.newInstance(account.server.url)
api.deposit(user.userId, amount)
sync(account.server, user)
sync(account.server, user, incremental = true)
} catch (e: Exception) {
Log.e(
"Sync",
......@@ -107,7 +111,7 @@ class SyncManager @Inject constructor(
val api = factory.newInstance(account.server.url)
try {
api.withdraw(user.userId, amount)
sync(account.server, user)
sync(account.server, user, incremental = true)
} catch (e: Exception) {
Log.e(
"Sync",
......
......@@ -33,18 +33,25 @@ import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.Transaction
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
class TransactionSyncHandler @Inject constructor(
private val factory: MeteApiFactory,
private val db: MeteroidDatabase,
private val repository: TransactionRepository
) : SyncHandler<Pair<Server, UserId>, Transaction, TransactionSyncHandler.Key>() {
) : BaseIncrementalSyncHandler<Pair<Server, UserId>, Transaction, TransactionSyncHandler.Key>() {
data class Key(
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)
override suspend fun store(entry: Transaction) =
......@@ -61,7 +68,36 @@ class TransactionSyncHandler @Inject constructor(
override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Transaction> {
val (server, userId) = context
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) }
}
}
......@@ -32,18 +32,19 @@ import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.sync.base.BaseSyncHandler
import javax.inject.Inject
class UserSyncHandler @Inject constructor(
private val factory: MeteApiFactory,
private val db: MeteroidDatabase,
private val repository: UserRepository
) : SyncHandler<Server, User, UserSyncHandler.Key>() {
) : BaseSyncHandler<Server, User, UserSyncHandler.Key>() {
data class Key(
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)
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 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.sync
package de.chaosdorf.meteroid.sync.base
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
abstract class SyncHandler<Context, Entry, Key> {
abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> {
sealed class State {
data object Idle : State()
data object Loading : State()
......@@ -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 loadStored(context: Context): List<Entry>
......@@ -55,15 +55,12 @@ abstract class SyncHandler<Context, Entry, Key> {
abstract suspend fun delete(key: Key)
abstract suspend fun store(entry: Entry)
private val _state = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> = _state
protected val syncState = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> = syncState
suspend fun sync(context: Context) {
if (_state.compareAndSet(State.Idle, State.Loading) || _state.compareAndSet(
State.Error(),
State.Loading
)
) {
override suspend fun sync(context: Context) {
if (syncState.compareAndSet(State.Idle, State.Loading) ||
syncState.compareAndSet(State.Error(), State.Loading)) {
Log.w(this::class.simpleName, "Started sync")
try {
val loadedEntries = loadCurrent(context)
......@@ -80,11 +77,11 @@ abstract class SyncHandler<Context, Entry, Key> {
store(loadedEntry)
}
}
_state.value = State.Idle
syncState.value = State.Idle
Log.w(this::class.simpleName, "Finished sync")
} catch (e: Exception) {
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 {
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)
}
......@@ -66,7 +66,7 @@ class AppViewModel @Inject constructor(
init {
accountProvider.account.distinctUntilChanged().onEach { account ->
account?.let { (server, maybeUser) ->
syncManager.sync(server, maybeUser)
syncManager.sync(server, maybeUser, incremental = true)
}
}.launchIn(viewModelScope)
}
......
......@@ -41,10 +41,10 @@ fun PriceBadge(
price: BigDecimal,
modifier: Modifier = Modifier,
containerColor: Color =
if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.primary
if (price >= BigDecimal.ZERO) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.error,
textColor: Color =
if (price > BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary
if (price >= BigDecimal.ZERO) MaterialTheme.colorScheme.onPrimary
else MaterialTheme.colorScheme.onError,
textStyle: TextStyle = MaterialTheme.typography.labelLarge
) {
......
......@@ -98,6 +98,14 @@ class DrinkListViewModel @Inject constructor(
}
}
fun sync() {
account.value?.let { (server, user) ->
viewModelScope.launch {
syncManager.sync(server, user, incremental = true)
}
}
}
suspend fun checkOffline(server: Server?): Boolean =
if (server == null) true
else syncManager.checkOffline(server)
......
......@@ -57,6 +57,7 @@ import coil.compose.rememberAsyncImagePainter
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.sample.SampleDrinkProvider
import de.chaosdorf.meteroid.ui.PriceBadge
import java.math.BigDecimal
@Preview(widthDp = 120, showBackground = true)
@Composable
......@@ -109,8 +110,13 @@ fun DrinkTile(
)
Spacer(Modifier.height(4.dp))
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
val unitPrice =
if (item.volume <= BigDecimal.ZERO) null
else item.price / item.volume
Text(
String.format("%.02fl · %.02f€/l", item.volume, item.price / item.volume),
if (unitPrice == null) String.format("%.02fl", item.volume)
else String.format("%.02fl · %.02f€/l", item.volume, item.price / item.volume),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
......
......@@ -36,7 +36,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.outlined.PushPin
import androidx.compose.material.icons.twotone.PushPin
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
......@@ -48,19 +47,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavOptions
import coil.compose.AsyncImage
import de.chaosdorf.meteroid.model.AccountInfo
import de.chaosdorf.meteroid.ui.PriceBadge
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.internal.toCanonicalHost
@Composable
fun MeteroidTopBar(
account: AccountInfo?,
onNavigate: (String, NavOptions) -> Unit,
onTogglePin: () -> Unit,
onTogglePin: () -> Unit
) {
Surface(
modifier = Modifier.padding(8.dp),
......@@ -83,35 +82,44 @@ fun MeteroidTopBar(
.background(MaterialTheme.colorScheme.tertiary)
)
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.align(Alignment.CenterVertically)) {
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.weight(1.0f, fill = true)
) {
if (account != null) {
if (account.user != null) {
Text(
account.user!!.name,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
Text(
account.server.url.toHttpUrl().host,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f),
fontWeight = FontWeight.Medium
fontWeight = FontWeight.Medium,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
} else {
Text(
account.server.url.toHttpUrl().host,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
}
} else {
Text(
"Meteroid",
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
overflow = TextOverflow.Ellipsis,
softWrap = false
)
}
}
Spacer(
Modifier
.weight(1.0f)
.width(16.dp))
Spacer(Modifier.width(16.dp))
IconButton(onClick = onTogglePin) {
Icon(
if (account?.pinned == true) Icons.Filled.PushPin
......
......@@ -24,6 +24,7 @@
package de.chaosdorf.meteroid.ui.transactions
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
......@@ -32,9 +33,7 @@ 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
import kotlinx.coroutines.flow.combine
......@@ -75,8 +74,10 @@ class TransactionViewModel @Inject constructor(
}
} ?: flowOf(emptyList())
}.mapLatest { list ->
list.mergeAdjecentDeposits()
.filter { it.drink != null || it.transaction.difference != BigDecimal.ZERO }
list.mergeAdjecentDeposits().filter {
it.drink != null ||
it.transaction.difference.abs() >= 0.01.toBigDecimal()
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
fun togglePin() {
......@@ -89,6 +90,14 @@ class TransactionViewModel @Inject constructor(
}
}
fun sync() {
account.value?.let { (server, user) ->
viewModelScope.launch {
syncManager.sync(server, user, incremental = true)
}
}
}
suspend fun checkOffline(server: Server?): Boolean =
if (server == null) true
else syncManager.checkOffline(server)
......
......@@ -95,7 +95,7 @@ sealed class WrappedSlide {
dosage,
Animal.values()
.sortedBy(Animal::lethalDosage)
.firstOrNull { it.lethalDosage < dosage }
.lastOrNull { it.lethalDosage < dosage }
)
}
}
......
......@@ -32,6 +32,7 @@ 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.model.Transaction
import de.chaosdorf.meteroid.model.TransactionRepository
import de.chaosdorf.meteroid.sync.AccountProvider
import de.chaosdorf.meteroid.sync.SyncManager
......@@ -60,6 +61,16 @@ class WrappedViewModel @Inject constructor(
val account: StateFlow<AccountInfo?> = accountProvider.account
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
private fun List<Transaction>.filterAudits(year: Int): List<Transaction> {
val yearBegin = LocalDateTime(year, Month.JANUARY, 1, 0, 0, 0)
.toInstant(TimeZone.UTC)
val yearEnd = LocalDateTime(year, Month.DECEMBER, 31, 23, 59, 59)
.toInstant(TimeZone.UTC)
return this.filter {
it.timestamp in yearBegin..yearEnd
}
}
val slides: StateFlow<List<WrappedSlide>> = accountProvider.account
.flatMapLatest { account ->
account?.let { (server, maybeUser) ->
......@@ -74,28 +85,9 @@ class WrappedViewModel @Inject constructor(
WrappedSlide.Caffeine,
WrappedSlide.MostActive
)
val timeZone = TimeZone.currentSystemDefault()
val now = Clock.System.now().toLocalDateTime(timeZone)
val yearBegin = LocalDateTime(
year = now.year,
month = Month.JANUARY,
dayOfMonth = 1,
hour = 0,
minute = 0,
second = 0
).toInstant(timeZone)
val yearEnd = LocalDateTime(
year = now.year + 1,
month = Month.JANUARY,
dayOfMonth = 1,
hour = 0,
minute = 0,
second = 0
).toInstant(timeZone)
val thisYear = transactions.filter {
it.timestamp in yearBegin..yearEnd
}
factories.mapNotNull { it.create(thisYear, drinkMap) }
val now = Clock.System.now().toLocalDateTime(TimeZone.UTC)
val content = transactions.filterAudits(now.year)
factories.mapNotNull { it.create(content, drinkMap) }
}
}
} ?: flowOf(emptyList())
......
[versions]
androidGradlePlugin = "8.1.3"
androidx-activity = "1.8.0"
androidGradlePlugin = "8.1.4"
androidx-activity = "1.8.1"
androidx-appcompat = "1.6.1"
androidx-compose-bom = "2023.10.01"
androidx-compose-compiler = "1.5.4"
androidx-compose-material = "1.5.0-alpha04"
androidx-compose-material3 = "1.2.0-alpha10"
androidx-compose-runtimetracing = "1.0.0-alpha04"
androidx-compose-tooling = "1.6.0-alpha08"
androidx-compose-material3 = "1.2.0-alpha11"
androidx-compose-runtimetracing = "1.0.0-alpha05"
androidx-compose-tooling = "1.6.0-beta01"
androidx-datastore = "1.0.0"
androidx-hilt = "1.1.0"
androidx-navigation = "2.7.5"
......
......@@ -86,6 +86,8 @@ interface TransactionRepository {
@Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC")
fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Transaction>>
@Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC LIMIT 1")
suspend fun getLatest(serverId: ServerId, userId: UserId): Transaction?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(transaction: Transaction)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment