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

refactor: recreate app structure

parent d7ceabe9
No related branches found
No related tags found
No related merge requests found
Showing
with 244 additions and 257 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.meteroid.ui.userlist
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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 androidx.navigation.NavController
import de.chaosdorf.meteroid.ui.MeteroidScreen
import de.chaosdorf.meteroid.util.findStartDestination
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun UserListScreen(
navController: NavController,
viewModel: UserListViewModel,
contentPadding: PaddingValues = PaddingValues(),
) {
val users by viewModel.users.collectAsState()
val pinnedUsers by viewModel.pinnedUsers.collectAsState()
LazyColumn(contentPadding = contentPadding) {
if (pinnedUsers.isNotEmpty()) {
stickyHeader("pinned") {
ListItem(headlineContent = {
Text(
"Pinned",
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 4.dp)
)
})
}
items(
pinnedUsers,
key = { "pinned-${it.serverId}-${it.userId}" },
) { user ->
UserListItem(user) { serverId, userId ->
navController.navigate(MeteroidScreen.Home.Purchase.build(serverId, userId)) {
launchSingleTop = true
restoreState = false
popUpTo(findStartDestination(navController.graph).id) {
saveState = false
}
}
viewModel.selectUser(serverId, userId)
}
}
}
for (character in 'A'..'Z') {
val group = users.filter { it.name.startsWith(character, ignoreCase = true) }
if (group.isNotEmpty()) {
stickyHeader(character.toString()) {
ListItem(headlineContent = {
Text(
"$character",
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 4.dp)
)
})
}
items(
group,
key = { "user-${it.serverId}-${it.userId}" },
) { user ->
UserListItem(user) { serverId, userId ->
navController.navigate(MeteroidScreen.Home.Purchase.build(serverId, userId)) {
launchSingleTop = true
restoreState = false
popUpTo(findStartDestination(navController.graph).id) {
saveState = false
}
}
viewModel.selectUser(serverId, userId)
}
}
}
}
}
}
/*
* 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.userlist
import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.model.PinnedUserRepository
import de.chaosdorf.meteroid.model.User
import de.chaosdorf.meteroid.model.UserRepository
import de.chaosdorf.meteroid.storage.AccountPreferences
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class UserListViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
userRepository: UserRepository,
pinnedUserRepository: PinnedUserRepository,
private val preferences: AccountPreferences,
) : ViewModel() {
private val serverId = ServerId(checkNotNull(savedStateHandle["server"]))
val users: StateFlow<List<User>> = userRepository.getAllFlow(serverId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val pinnedUsers = combine(
userRepository.getAllFlow(serverId),
pinnedUserRepository.getAllFlow(serverId)
) { users, pinned ->
users.filter { user -> pinned.contains(user.userId) }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
fun selectUser(serverId: ServerId, userId: UserId) {
Log.i("UserListViewModel", "Updating AccountPreferences: $serverId $userId")
viewModelScope.launch {
preferences.setUser(serverId, userId)
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
* Copyright (c) 2013-2025 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
......@@ -22,28 +22,36 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid
package de.chaosdorf.meteroid.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.storage.AccountPreferences
import de.chaosdorf.meteroid.sync.SyncManager
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MeteroidViewModel @Inject constructor(
accountPreferences: AccountPreferences,
@AssistedFactory
interface InitViewModelFactory {
fun create(): InitViewModel
}
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel(assistedFactory = InitViewModelFactory::class)
class InitViewModel @AssistedInject constructor(
serverRepository: ServerRepository,
syncManager: SyncManager
syncManager: SyncManager,
) : ViewModel() {
val initialAccount: StateFlow<AccountPreferences.State?> = accountPreferences.state
.filterNotNull()
.take(1)
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
val setupComplete = serverRepository.getAllFlow()
.mapLatest { it.isNotEmpty() }
.stateIn(viewModelScope, WhileSubscribed(), null)
init {
viewModelScope.launch {
......
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2025 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.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.meteroid.model.ServerRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
@AssistedFactory
interface ServerListViewModelFactory {
fun create(): ServerListViewModel
}
@HiltViewModel(assistedFactory = ServerListViewModelFactory::class)
class ServerListViewModel @AssistedInject constructor(
serverRepository: ServerRepository,
) : ViewModel() {
val servers = serverRepository.getAllFlow()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
* Copyright (c) 2013-2025 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
......@@ -22,11 +22,20 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui.settings
package de.chaosdorf.meteroid.viewmodel
import androidx.lifecycle.ViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
@HiltViewModel
class SettingsViewModel @Inject constructor() : ViewModel()
@AssistedFactory
interface SettingsViewModelFactory {
fun create(): SettingsViewModel
}
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel(assistedFactory = SettingsViewModelFactory::class)
class SettingsViewModel @AssistedInject constructor() : ViewModel() {
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
* Copyright (c) 2013-2025 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
......@@ -22,38 +22,46 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui.servers
package de.chaosdorf.meteroid.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.model.MeteApiFactory
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.sync.SyncManager
import de.chaosdorf.meteroid.util.newServer
import kotlinx.coroutines.flow.*
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.MutableStateFlow
@HiltViewModel
class AddServerViewModel @Inject constructor(
private val factory: MeteApiFactory,
private val repository: ServerRepository
) : ViewModel() {
val url = MutableStateFlow("")
@AssistedFactory
interface SetupViewModelFactory {
fun create(): SetupViewModel
}
val server: StateFlow<Server?> = url
.debounce(300.milliseconds)
.mapLatest { factory.newServer(ServerId(-1), it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
@HiltViewModel(assistedFactory = SetupViewModelFactory::class)
class SetupViewModel @AssistedInject constructor(
private val apiFactory: MeteApiFactory,
private val serverRepository: ServerRepository,
private val syncManager: SyncManager,
) : ViewModel() {
val serverUrl = MutableStateFlow("http://192.168.1.41:8080")
val error = MutableStateFlow<String?>(null)
suspend fun addServer() {
val highestServerId = repository.getAll().maxOfOrNull { it.serverId.value } ?: 0
suspend fun add(): Server? {
val highestServerId = serverRepository.getAll().maxOfOrNull { it.serverId.value } ?: 0
val serverId = ServerId(highestServerId + 1)
val server = factory.newServer(serverId, url.value)
val server = apiFactory.newServer(serverId, serverUrl.value)
if (server != null) {
repository.save(server)
serverRepository.save(server)
syncManager.sync(server, null, incremental = false)
error.value = null
return server
} else {
error.value = "Server not found"
return null
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
* Copyright (c) 2013-2025 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
......@@ -22,38 +22,34 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui.home.deposit
package de.chaosdorf.meteroid.viewmodel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.mete.model.UserId
import de.chaosdorf.meteroid.sync.AccountProvider
import de.chaosdorf.meteroid.sync.SyncManager
import kotlinx.coroutines.launch
import javax.inject.Inject
import de.chaosdorf.meteroid.model.UserRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
@HiltViewModel
class DepositViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val accountProvider: AccountProvider,
private val syncManager: SyncManager,
) : ViewModel() {
private val serverId = ServerId(checkNotNull(savedStateHandle["server"]))
private val userId = UserId(checkNotNull(savedStateHandle["user"]))
val money: List<MonetaryAmount> = MonetaryAmount.entries
fun deposit(item: MonetaryAmount, onBack: () -> Unit) {
viewModelScope.launch {
accountProvider.account(serverId, userId)?.let { account ->
syncManager.deposit(account, item.amount)
if (!account.pinned) {
onBack()
}
}
}
@AssistedFactory
interface UserListViewModelFactory {
fun create(
@Assisted("serverId") serverId: Long,
): UserListViewModel
}
@HiltViewModel(assistedFactory = UserListViewModelFactory::class)
class UserListViewModel @AssistedInject constructor(
@Assisted("serverId") serverId: Long,
userRepository: UserRepository,
) : ViewModel() {
val serverId: ServerId = ServerId(serverId)
val users = userRepository.getAllFlow(this.serverId)
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2023 Chaosdorf e.V.
* Copyright (c) 2013-2025 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
......@@ -22,12 +22,29 @@
* THE SOFTWARE.
*/
package de.chaosdorf.meteroid.ui.home.transactionhistory
package de.chaosdorf.meteroid.viewmodel
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.Transaction
import androidx.lifecycle.ViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.mete.model.UserId
data class TransactionInfo(
val transaction: Transaction,
val drink: Drink?
)
@AssistedFactory
interface UserViewModelFactory {
fun create(
@Assisted("serverId") serverId: Long,
@Assisted("userId") userId: Long,
): UserViewModel
}
@HiltViewModel(assistedFactory = UserViewModelFactory::class)
class UserViewModel @AssistedInject constructor(
@Assisted("serverId") serverId: Long,
@Assisted("userId") userId: Long,
) : ViewModel() {
val serverId: ServerId = ServerId(serverId)
val userId: UserId = UserId(userId)
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2013-2025 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.viewmodel
import androidx.lifecycle.ViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.model.ServerId
import de.chaosdorf.mete.model.UserId
@AssistedFactory
interface WrappedViewModelFactory {
fun create(
@Assisted("serverId") serverId: Long,
@Assisted("userId") userId: Long,
): WrappedViewModel
}
@HiltViewModel(assistedFactory = WrappedViewModelFactory::class)
class WrappedViewModel @AssistedInject constructor(
@Assisted("serverId") serverId: Long,
@Assisted("userId") userId: Long,
) : ViewModel() {
val serverId: ServerId = ServerId(serverId)
val userId: UserId = UserId(userId)
}
......@@ -16,11 +16,11 @@ class AndroidApplicationConvention : Plugin<Project> {
}
extensions.configure<ApplicationExtension> {
compileSdk = 35
compileSdk = 36
defaultConfig {
minSdk = 21
targetSdk = 35
targetSdk = 36
applicationId = "${rootProject.group}.${rootProject.name.lowercase(Locale.ROOT)}"
versionCode = cmd("git", "rev-list", "--count", "HEAD")?.toIntOrNull() ?: 1
......
......@@ -12,7 +12,7 @@ class AndroidLibraryConvention : Plugin<Project> {
}
extensions.configure<LibraryExtension> {
compileSdk = 35
compileSdk = 36
defaultConfig {
minSdk = 21
......
......@@ -11,7 +11,8 @@ androidx-appcompat = "1.7.1"
androidx-compose = "1.8.2"
androidx-compose-compiler = "1.5.15"
androidx-datastore = "1.1.7"
androidx-navigation = "2.9.0"
androidx-navigation3 = "1.0.0-alpha03"
androidx-navigation3-viewmodel = "1.0.0-alpha01"
androidx-room = "2.7.1"
androidx-splashscreen = "1.0.1"
androidx-material3 = "1.3.2"
......@@ -62,7 +63,9 @@ okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-converter-kotlinx = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" }
androidx-navigation3-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-navigation3-viewmodel" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" }
junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
junit-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment