Skip to content
Snippets Groups Projects
Unverified Commit 21a9bafd authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

wip: more functionality and cleanup

parent 00c1e75b
Branches
No related tags found
No related merge requests found
Showing
with 665 additions and 99 deletions
/*
* 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.mete
import kotlinx.serialization.Serializable
@Serializable
@JvmInline
value class AuditEntryId(val value: Long)
...@@ -24,13 +24,17 @@ ...@@ -24,13 +24,17 @@
package de.chaosdorf.mete.v1 package de.chaosdorf.mete.v1
import de.chaosdorf.mete.AuditEntryId
import de.chaosdorf.mete.DrinkId import de.chaosdorf.mete.DrinkId
import de.chaosdorf.mete.UserId
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class AuditEntryModelV1( data class AuditEntryModelV1(
val id: AuditEntryId,
@SerialName("created_at")
val createdAt: Instant,
val difference: Double, val difference: Double,
val drink: DrinkId, val drink: DrinkId?
val user: UserId,
val createdAt: Instant
) )
/*
* 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.mete.v1
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class AuditResponseV1(
@SerialName("payments_sum")
val payments: Double,
@SerialName("deposits_sum")
val deposits: Double,
@SerialName("sum")
val total: Double,
@SerialName("audits")
val entries: List<AuditEntryModelV1>
)
...@@ -29,13 +29,16 @@ import de.chaosdorf.mete.PwaManifest ...@@ -29,13 +29,16 @@ import de.chaosdorf.mete.PwaManifest
import de.chaosdorf.mete.UserId import de.chaosdorf.mete.UserId
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query
interface MeteApiV1 { interface MeteApiV1 {
@GET("manifest.json") @GET("manifest.json")
suspend fun getManifest(): PwaManifest? suspend fun getManifest(): PwaManifest?
@GET("api/v1/audits.json") @GET("api/v1/audits.json")
suspend fun getAudits(): List<AuditEntryModelV1> suspend fun getAudits(
@Query("user") user: Long? = null
): AuditResponseV1
@GET("api/v1/barcodes.json") @GET("api/v1/barcodes.json")
suspend fun listBarcodes(): List<BarcodeModelV1> suspend fun listBarcodes(): List<BarcodeModelV1>
......
...@@ -56,6 +56,9 @@ android { ...@@ -56,6 +56,9 @@ android {
dependencies { dependencies {
implementation(libs.kotlin.stdlib) implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
coreLibraryDesugaring(libs.desugar.jdk)
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.kotlinx.coroutines.test)
...@@ -86,14 +89,13 @@ dependencies { ...@@ -86,14 +89,13 @@ dependencies {
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.kotlinx.serialization.json)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.hilt.navigation) implementation(libs.hilt.navigation)
implementation(libs.hilt.android) implementation(libs.hilt.android)
ksp(libs.hilt.compiler) ksp(libs.hilt.compiler)
implementation("androidx.datastore:datastore-preferences:1.0.0") implementation(libs.androidx.datastore.preferences)
implementation(project(":api")) implementation(project(":api"))
implementation(project(":persistence")) implementation(project(":persistence"))
......
...@@ -33,6 +33,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext ...@@ -33,6 +33,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import de.chaosdorf.meteroid.MeteroidDatabase import de.chaosdorf.meteroid.MeteroidDatabase
import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.model.PurchaseRepository
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 javax.inject.Singleton import javax.inject.Singleton
...@@ -59,6 +60,11 @@ object DatabaseModule { ...@@ -59,6 +60,11 @@ object DatabaseModule {
database: MeteroidDatabase database: MeteroidDatabase
): UserRepository = database.users() ): UserRepository = database.users()
@Provides
fun providePurchaseRepository(
database: MeteroidDatabase
): PurchaseRepository = database.purchases()
@Provides @Provides
fun provideServerRepository( fun provideServerRepository(
database: MeteroidDatabase database: MeteroidDatabase
......
/*
* 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.sample
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import de.chaosdorf.mete.DrinkId
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.ServerId
import kotlinx.datetime.Instant
class SampleDrinkProvider : PreviewParameterProvider<Drink> {
override val values = sequenceOf(
Drink(
serverId = ServerId(-1),
drinkId = DrinkId(27),
active = true,
name = "Club Mate",
volume = 0.5,
caffeine = null,
price = 1.5,
createdAt = Instant.fromEpochMilliseconds(1684598011800),
updatedAt = Instant.fromEpochMilliseconds(1684607122132),
logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png",
logoFileName = "logo.png",
logoContentType = "image/png",
logoFileSize = 183063,
logoUpdatedAt = Instant.fromEpochMilliseconds(1684607121995)
),
Drink(
serverId = ServerId(-1),
drinkId = DrinkId(15),
active = false,
name = "Paulaner Spezi",
volume = 0.5,
caffeine = null,
price = 1.5,
createdAt = Instant.fromEpochMilliseconds(1684597806099),
updatedAt = Instant.fromEpochMilliseconds(1684607346944),
logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png",
logoFileName = "logo.png",
logoContentType = "image/png",
logoFileSize = 173265,
logoUpdatedAt = Instant.fromEpochMilliseconds(1684607346835)
)
)
}
/*
* 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
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.storage.AccountPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject
class AccountProvider @Inject constructor(
accountPreferences: AccountPreferences,
serverRepository: ServerRepository,
userRepository: UserRepository,
) {
val account: Flow<Pair<Server, User?>?> =
accountPreferences.state.flatMapLatest { preferences ->
if (preferences.server == null) {
flowOf(null)
} else {
serverRepository.getFlow(preferences.server).flatMapLatest { server ->
if (server == null) {
flowOf(null)
} else if (preferences.user == null) {
flowOf(Pair(server, null))
} else {
userRepository.getFlow(server.serverId, preferences.user)
.mapLatest { user -> Pair(server, user) }
}
}
}
}
}
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.storage package de.chaosdorf.meteroid.sync
import androidx.room.withTransaction import androidx.room.withTransaction
import de.chaosdorf.mete.DrinkId import de.chaosdorf.mete.DrinkId
...@@ -59,6 +59,6 @@ class DrinkSyncHandler @Inject constructor( ...@@ -59,6 +59,6 @@ class DrinkSyncHandler @Inject constructor(
override suspend fun loadCurrent(context: Server): List<Drink> { override suspend fun loadCurrent(context: Server): List<Drink> {
val api = MeteApiV1Factory.newInstance(context.url) val api = MeteApiV1Factory.newInstance(context.url)
val loadedEntries = api.listDrinks() val loadedEntries = api.listDrinks()
return loadedEntries.map { Drink.fromModelV1(context.serverId, it) } return loadedEntries.map { Drink.fromModelV1(context, it) }
} }
} }
/*
* 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
import androidx.room.withTransaction
import de.chaosdorf.mete.AuditEntryId
import de.chaosdorf.mete.UserId
import de.chaosdorf.mete.v1.MeteApiV1Factory
import de.chaosdorf.meteroid.MeteroidDatabase
import de.chaosdorf.meteroid.model.Purchase
import de.chaosdorf.meteroid.model.PurchaseRepository
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import javax.inject.Inject
class PurchaseSyncHandler @Inject constructor(
private val db: MeteroidDatabase,
private val repository: PurchaseRepository
) : SyncHandler<Pair<Server, UserId>, Purchase, PurchaseSyncHandler.Key>() {
data class Key(
val server: ServerId, val purchase: AuditEntryId
)
override suspend fun withTransaction(block: suspend () -> Unit) =
db.withTransaction(block)
override suspend fun store(entry: Purchase) =
repository.save(entry)
override suspend fun delete(key: Key) =
repository.delete(key.server, key.purchase)
override fun entryToKey(entry: Purchase) = Key(entry.serverId, entry.purchaseId)
override suspend fun loadStored(context: Pair<Server, UserId>): List<Purchase> =
repository.getAll(context.first.serverId, context.second)
override suspend fun loadCurrent(context: Pair<Server, UserId>): List<Purchase> {
val (server, userId) = context
val api = MeteApiV1Factory.newInstance(server.url)
val loadedEntries = api.getAudits(user = userId.value).entries
return loadedEntries.map { Purchase.fromModelV1(server, userId, it) }
}
}
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.storage package de.chaosdorf.meteroid.sync
import android.util.Log import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
......
/*
* 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
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SyncViewModel @Inject constructor(
) : ViewModel() {
}
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.storage package de.chaosdorf.meteroid.sync
import androidx.room.withTransaction import androidx.room.withTransaction
import de.chaosdorf.mete.UserId import de.chaosdorf.mete.UserId
...@@ -59,6 +59,6 @@ class UserSyncHandler @Inject constructor( ...@@ -59,6 +59,6 @@ class UserSyncHandler @Inject constructor(
override suspend fun loadCurrent(context: Server): List<User> { override suspend fun loadCurrent(context: Server): List<User> {
val api = MeteApiV1Factory.newInstance(context.url) val api = MeteApiV1Factory.newInstance(context.url)
val loadedEntries = api.listUsers() val loadedEntries = api.listUsers()
return loadedEntries.map { User.fromModelV1(context.serverId, it) } return loadedEntries.map { User.fromModelV1(context, it) }
} }
} }
...@@ -25,23 +25,28 @@ ...@@ -25,23 +25,28 @@
package de.chaosdorf.meteroid.ui package de.chaosdorf.meteroid.ui
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.Column
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
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.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Alignment
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
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.navigation
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import de.chaosdorf.meteroid.ui.home.DrinkListScreen import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen
import de.chaosdorf.meteroid.ui.home.DrinkListViewModel import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel
import de.chaosdorf.meteroid.ui.home.HomeSections import de.chaosdorf.meteroid.ui.money.MoneyListScreen
import de.chaosdorf.meteroid.ui.money.MoneyListViewModel
import de.chaosdorf.meteroid.ui.navigation.Routes
import de.chaosdorf.meteroid.ui.purchases.PurchaseListScreen
import de.chaosdorf.meteroid.ui.purchases.PurchaseViewModel
import de.chaosdorf.meteroid.ui.servers.AddServerScreen import de.chaosdorf.meteroid.ui.servers.AddServerScreen
import de.chaosdorf.meteroid.ui.servers.ServerListScreen import de.chaosdorf.meteroid.ui.servers.ServerListScreen
import de.chaosdorf.meteroid.ui.users.UserListScreen import de.chaosdorf.meteroid.ui.users.UserListScreen
...@@ -65,10 +70,13 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) { ...@@ -65,10 +70,13 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) {
NavHost(navController, startDestination = Routes.Init) { NavHost(navController, startDestination = Routes.Init) {
composable(route = Routes.Init) { _ -> composable(route = Routes.Init) { _ ->
Box { Box(contentAlignment = Alignment.Center) {
Column {
CircularProgressIndicator()
Text("Loading") Text("Loading")
} }
} }
}
navigation(route = Routes.Servers.Root, startDestination = Routes.Servers.List) { navigation(route = Routes.Servers.Root, startDestination = Routes.Servers.List) {
composable(Routes.Servers.List) { _ -> composable(Routes.Servers.List) { _ ->
...@@ -116,26 +124,15 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) { ...@@ -116,26 +124,15 @@ fun AppRouter(viewModel: AppViewModel = viewModel()) {
navigation(route = Routes.Home.Root, startDestination = Routes.Home.Purchase) { navigation(route = Routes.Home.Root, startDestination = Routes.Home.Purchase) {
composable(Routes.Home.Purchase) { _ -> composable(Routes.Home.Purchase) { _ ->
val drinkListViewModel = hiltViewModel<DrinkListViewModel>() val drinkListViewModel = hiltViewModel<DrinkListViewModel>()
MeteroidScaffold( DrinkListScreen(drinkListViewModel, navController::navigate)
routes = HomeSections.entries,
currentRoute = HomeSections.PURCHASE,
navigateTo = navController::navigate,
onBack = { navController.navigate(Routes.Users.Root) },
) { paddingValues ->
DrinkListScreen(drinkListViewModel, Modifier.padding(paddingValues))
}
} }
composable(Routes.Home.Deposit) { _ -> composable(Routes.Home.Deposit) { _ ->
MeteroidScaffold( val moneyListViewModel = hiltViewModel<MoneyListViewModel>()
routes = HomeSections.entries, MoneyListScreen(moneyListViewModel, navController::navigate)
currentRoute = HomeSections.DEPOSIT,
navigateTo = navController::navigate,
onBack = { navController.navigate(Routes.Users.Root) },
) { paddingValues ->
Box(Modifier.padding(paddingValues)) {
Text("TODO: Deposit")
}
} }
composable(Routes.Home.History) { _ ->
val purchaseViewModel = hiltViewModel<PurchaseViewModel>()
PurchaseListScreen(purchaseViewModel, navController::navigate)
} }
} }
} }
......
...@@ -32,12 +32,14 @@ import de.chaosdorf.meteroid.model.Server ...@@ -32,12 +32,14 @@ import de.chaosdorf.meteroid.model.Server
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.storage.AccountPreferences import de.chaosdorf.meteroid.storage.AccountPreferences
import de.chaosdorf.meteroid.storage.DrinkSyncHandler import de.chaosdorf.meteroid.sync.AccountProvider
import de.chaosdorf.meteroid.storage.UserSyncHandler import de.chaosdorf.meteroid.sync.DrinkSyncHandler
import de.chaosdorf.meteroid.sync.PurchaseSyncHandler
import de.chaosdorf.meteroid.sync.UserSyncHandler
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
...@@ -46,10 +48,12 @@ import javax.inject.Inject ...@@ -46,10 +48,12 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AppViewModel @Inject constructor( class AppViewModel @Inject constructor(
accountProvider: AccountProvider,
private val accountPreferences: AccountPreferences, private val accountPreferences: AccountPreferences,
private val serverRepository: ServerRepository, private val serverRepository: ServerRepository,
private val userSyncHandler: UserSyncHandler, private val userSyncHandler: UserSyncHandler,
private val drinkSyncHandler: DrinkSyncHandler, private val drinkSyncHandler: DrinkSyncHandler,
private val purchaseSyncHandler: PurchaseSyncHandler
) : ViewModel() { ) : ViewModel() {
val initState: StateFlow<InitState> = accountPreferences.state val initState: StateFlow<InitState> = accountPreferences.state
.flatMapLatest { preferences -> .flatMapLatest { preferences ->
...@@ -63,17 +67,14 @@ class AppViewModel @Inject constructor( ...@@ -63,17 +67,14 @@ class AppViewModel @Inject constructor(
} }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), InitState.LOADING) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), InitState.LOADING)
private val server: StateFlow<Server?> =
accountPreferences.state.flatMapLatest { preferences ->
if (preferences.server == null) flowOf(null)
else serverRepository.getFlow(preferences.server)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
init { init {
server.onEach { server -> accountProvider.account.distinctUntilChanged().onEach { account ->
if (server != null) { account?.let { (server, user) ->
userSyncHandler.sync(server) userSyncHandler.sync(server)
drinkSyncHandler.sync(server) drinkSyncHandler.sync(server)
user?.let { user ->
purchaseSyncHandler.sync(Pair(server, user.userId))
}
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
} }
......
/*
* 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.drinks
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
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.LinearProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.chaosdorf.meteroid.sync.SyncHandler
import de.chaosdorf.meteroid.ui.navigation.HomeSections
import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
@Composable
fun DrinkListScreen(
viewModel: DrinkListViewModel,
onNavigate: (String) -> Unit = {}
) {
val account by viewModel.account.collectAsState()
val drinks by viewModel.drinks.collectAsState()
val syncState by viewModel.syncState.collectAsState()
Scaffold(
topBar = { MeteroidTopBar(account, onNavigate) },
bottomBar = {
MeteroidBottomBar(
currentRoute = HomeSections.PURCHASE,
historyEnabled = account?.second?.audit == true,
navigateTo = onNavigate
)
}
) { paddingValues: PaddingValues ->
Column {
if (syncState == SyncHandler.State.Loading) {
LinearProgressIndicator()
}
LazyVerticalGrid(
GridCells.Adaptive(120.dp),
modifier = Modifier.padding(paddingValues),
contentPadding = PaddingValues(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
items(drinks) { drink ->
DrinkTile(drink)
}
}
}
}
}
...@@ -22,37 +22,40 @@ ...@@ -22,37 +22,40 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui.home package de.chaosdorf.meteroid.ui.drinks
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.meteroid.model.Drink import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.storage.AccountPreferences import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.storage.DrinkSyncHandler import de.chaosdorf.meteroid.sync.AccountProvider
import kotlinx.coroutines.flow.Flow import de.chaosdorf.meteroid.sync.DrinkSyncHandler
import de.chaosdorf.meteroid.sync.SyncHandler
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class DrinkListViewModel @Inject constructor( class DrinkListViewModel @Inject constructor(
drinkRepository: DrinkRepository, accountProvider: AccountProvider,
accountPreferences: AccountPreferences, repository: DrinkRepository,
syncHandler: DrinkSyncHandler syncHandler: DrinkSyncHandler
) : ViewModel() { ) : ViewModel() {
private val serverId: Flow<ServerId?> = accountPreferences.state.mapLatest { it.server } val account: StateFlow<Pair<Server, User?>?> = accountProvider.account
val drinks: StateFlow<List<Drink>> = serverId.flatMapLatest { .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
it?.let { serverId ->
drinkRepository.getAllFlow(serverId) val drinks: StateFlow<List<Drink>> = accountProvider.account
.flatMapLatest { account ->
account?.let { (server, _) ->
repository.getAllFlow(server.serverId)
} ?: flowOf(emptyList()) } ?: flowOf(emptyList())
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val syncState = syncHandler.state val syncState: StateFlow<SyncHandler.State> = syncHandler.state
} }
/*
* 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.drinks
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.sample.SampleDrinkProvider
@Preview(widthDp = 120, showBackground = true)
@Composable
fun DrinkTile(
@PreviewParameter(SampleDrinkProvider::class) item: Drink
) {
val thumbPainter = rememberAsyncImagePainter(
item.logoUrl
)
val drinkPainter = rememberAsyncImagePainter(
item.logoUrl.replace("/thumb/", "/original/"),
error = thumbPainter
)
Column(
modifier = Modifier.padding(4.dp)
) {
Box {
Image(
drinkPainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
.aspectRatio(1.0f)
.padding(8.dp)
)
Text(
String.format("%.02f €", item.price),
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(vertical = 12.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.primary)
.align(Alignment.BottomEnd)
.padding(horizontal = 8.dp)
)
}
Text(
item.name,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp)
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(horizontal = 4.dp)
.fillMaxWidth()
) {
Text(
String.format("%.02f l", item.volume),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
)
}
}
}
...@@ -22,53 +22,53 @@ ...@@ -22,53 +22,53 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid.ui package de.chaosdorf.meteroid.ui.money
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material.icons.Icons import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.icons.filled.Menu import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import de.chaosdorf.meteroid.ui.navigation.HomeSections
import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar
import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar
@Composable @Composable
fun <T : MeteroidNavSection> MeteroidScaffold( fun MoneyListScreen(
routes: Iterable<T>, viewModel: MoneyListViewModel,
currentRoute: T, onNavigate: (String) -> Unit = {}
navigateTo: (String) -> Unit,
onBack: () -> Unit,
content: @Composable (PaddingValues) -> Unit
) { ) {
val account by viewModel.account.collectAsState()
Scaffold( Scaffold(
topBar = { topBar = { MeteroidTopBar(account, onNavigate) },
TopAppBar(
title = { Text("Meteroid") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
},
modifier = Modifier
.padding(8.dp)
.shadow(4.dp, shape = RoundedCornerShape(8.dp))
.zIndex(1.0f)
)
},
bottomBar = { bottomBar = {
NavigationBar { MeteroidBottomBar(
MeteroidNavSections(routes, currentRoute, navigateTo) currentRoute = HomeSections.DEPOSIT,
} historyEnabled = account?.second?.audit == true,
}, navigateTo = onNavigate
content = content
) )
} }
) { paddingValues: PaddingValues ->
Column {
LazyVerticalGrid(
GridCells.Adaptive(120.dp),
modifier = Modifier.padding(paddingValues),
contentPadding = PaddingValues(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
items(viewModel.money) { monetaryAmount ->
MoneyTile(monetaryAmount)
}
}
}
}
}
/*
* 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.money
import androidx.annotation.DrawableRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.meteroid.R
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.sync.AccountProvider
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
enum class MonetaryAmount(val amount: Double, @DrawableRes val image: Int) {
MONEY_50(0.50, R.drawable.euro_50),
MONEY_100(1.00, R.drawable.euro_100),
MONEY_200(2.00, R.drawable.euro_200),
MONEY_500(5.00, R.drawable.euro_500),
MONEY_1000(10.00, R.drawable.euro_1000),
MONEY_2000(20.00, R.drawable.euro_2000),
MONEY_5000(50.00, R.drawable.euro_5000),
}
@HiltViewModel
class MoneyListViewModel @Inject constructor(
accountProvider: AccountProvider
) : ViewModel() {
val account: StateFlow<Pair<Server, User?>?> = accountProvider.account
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val money: List<MonetaryAmount> = MonetaryAmount.entries
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment