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

wip: cleanup structure and navigation

parent 803d14f4
No related branches found
No related tags found
No related merge requests found
Showing
with 941 additions and 346 deletions
...@@ -26,6 +26,7 @@ package de.chaosdorf.mete.v1 ...@@ -26,6 +26,7 @@ package de.chaosdorf.mete.v1
import de.chaosdorf.mete.UserId import de.chaosdorf.mete.UserId
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
...@@ -33,7 +34,9 @@ data class UserModelV1( ...@@ -33,7 +34,9 @@ data class UserModelV1(
val id: UserId, val id: UserId,
val name: String, val name: String,
val email: String, val email: String,
@SerialName("created_at")
val createdAt: Instant, val createdAt: Instant,
@SerialName("updated_at")
val updatedAt: Instant, val updatedAt: Instant,
val balance: Double, val balance: Double,
val active: Boolean, val active: Boolean,
......
...@@ -27,99 +27,15 @@ package de.chaosdorf.meteroid ...@@ -27,99 +27,15 @@ 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.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.twotone.Money
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import coil.compose.AsyncImage
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.meteroid.ui.AppRouter
import de.chaosdorf.mete.v1.MeteApiV1Factory
import de.chaosdorf.meteroid.icons.MeteroidIcons
import de.chaosdorf.meteroid.icons.twotone.WaterFull
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.model.ServerRepository
import de.chaosdorf.meteroid.ui.theme.MeteroidTheme import de.chaosdorf.meteroid.ui.theme.MeteroidTheme
import de.chaosdorf.meteroid.util.findBestIcon
import de.chaosdorf.meteroid.util.resolve
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@Inject
lateinit var accountPreferences: AccountPreferences
@Inject
lateinit var serverRepository: ServerRepository
@Inject
lateinit var drinkSyncHandler: DrinkSyncHandler
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val server = accountPreferences.state
.mapLatest { it.server }
.flatMapLatest {
it?.let { serverRepository.getFlow(it) }
?: flowOf<Server?>(null)
}
lifecycleScope.launch {
server.collectLatest {
it?.let { server ->
drinkSyncHandler.sync(server)
}
}
}
setContent { setContent {
MeteroidTheme { MeteroidTheme {
AppRouter() AppRouter()
...@@ -127,247 +43,3 @@ class MainActivity : ComponentActivity() { ...@@ -127,247 +43,3 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
@HiltViewModel
class AppViewModel @Inject constructor(
private val accountPreferences: AccountPreferences,
private val serverRepository: ServerRepository,
private val drinkSyncHandler: DrinkSyncHandler
) : ViewModel() {
val server: StateFlow<Server?> = accountPreferences.state
.mapLatest { it.server }
.flatMapLatest { serverId ->
serverRepository.getAllFlow()
.mapLatest { list ->
list.firstOrNull { server -> server.serverId == serverId }
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
init {
viewModelScope.launch {
server.collectLatest {
it?.let { server ->
drinkSyncHandler.sync(server)
}
}
}
}
suspend fun selectServer(server: ServerId) {
accountPreferences.setServer(server)
}
}
@Composable
fun AppRouter(viewModel: AppViewModel = viewModel()) {
val scope = rememberCoroutineScope()
val server by viewModel.server.collectAsState()
val navController = rememberNavController()
LaunchedEffect(server) {
if (server == null) {
navController.navigate("accounts/list")
}
}
Scaffold(topBar = {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
if (currentDestination?.hierarchy?.any { it.route == "main" } == true) {
TopAppBar(title = { Text("Meteroid") }, navigationIcon = {
IconButton(onClick = { navController.navigate("accounts") }) {
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null)
}
})
} else if (currentDestination?.hierarchy?.any { it.route == "accounts/list" } == true) {
TopAppBar(title = { Text("Meteroid") }, navigationIcon = {
IconButton(onClick = { navController.navigate("main") }) {
Icon(Icons.Default.Close, contentDescription = null)
}
})
}
}, bottomBar = {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
if (currentDestination?.hierarchy?.any { it.route == "main" } == true) {
BottomAppBar(actions = {
IconButton(onClick = { navController.navigate("main/purchase") }) {
Icon(MeteroidIcons.TwoTone.WaterFull, contentDescription = null)
}
IconButton(onClick = { navController.navigate("main/deposit") }) {
Icon(Icons.TwoTone.Money, contentDescription = null)
}
})
}
}) { padding ->
NavHost(navController, startDestination = "main", Modifier.padding(padding)) {
navigation(route = "accounts", startDestination = "accounts/list") {
composable("accounts/list") { backStackEntry ->
ServerListScreen(
hiltViewModel(),
onAddServer = { navController.navigate("accounts/addServer") },
onSelectServer = {
scope.launch {
viewModel.selectServer(it)
navController.navigate("main")
}
}
)
}
composable("accounts/addServer") { backStackEntry ->
AddServerScreen(
hiltViewModel(),
onAddServer = { navController.navigate("accounts/list") }
)
}
}
navigation(route = "main", startDestination = "main/purchase") {
/*
composable("users") { backStackEntry ->
val viewModel = hiltViewModel<UserListViewModel>()
UserListScreen(viewModel)
}
*/
composable("main/purchase") { backStackEntry ->
DrinkListScreen(hiltViewModel())
}
composable("main/deposit") { backStackEntry ->
}
}
}
}
}
@HiltViewModel
class AddServerViewModel @Inject constructor(
private val repository: ServerRepository
) : ViewModel() {
val url = MutableStateFlow("")
private suspend fun buildServer(
id: ServerId,
url: String
): Server? = try {
val api = MeteApiV1Factory.newInstance(url)
val manifest = api.getManifest()
val icon = manifest?.findBestIcon()
Server(
id,
manifest?.name,
url,
icon?.resolve(url)
)
} catch (_: Exception) {
null
}
val server: StateFlow<Server?> = url
.debounce(300.milliseconds)
.mapLatest { buildServer(ServerId(-1), it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
suspend fun addServer() {
val highestServerId = repository.getAll().maxOfOrNull { it.serverId.value } ?: 0
val serverId = ServerId(highestServerId + 1)
val server = buildServer(serverId, url.value)
if (server != null) {
repository.save(server)
}
}
}
@Composable
fun AddServerScreen(viewModel: AddServerViewModel, onAddServer: () -> Unit) {
val scope = rememberCoroutineScope()
val url by viewModel.url.collectAsState()
val server by viewModel.server.collectAsState()
Column {
TextField(
label = { Text("Server URL") },
value = url,
onValueChange = { viewModel.url.value = it }
)
Button(onClick = {
scope.launch {
viewModel.addServer()
onAddServer()
}
}) {
Text("Add Server")
}
server?.let { server ->
Text(server.url)
Text(server.name ?: "null1")
AsyncImage(model = server.logoUrl, contentDescription = null)
}
}
}
@Preview
@Composable
fun ServerListScreen(
viewModel: ServerListViewModel = viewModel(),
onAddServer: () -> Unit = {},
onSelectServer: (ServerId) -> Unit = {}
) {
val servers by viewModel.servers.collectAsState()
LazyColumn {
items(servers) { server ->
ListItem(
headlineContent = { Text(server.name ?: server.url) },
modifier = Modifier.clickable {
onSelectServer(server.serverId)
}
)
}
item {
ListItem(
headlineContent = { Text("Add Server") },
modifier = Modifier.clickable {
onAddServer()
}
)
}
}
}
@Preview
@Composable
fun DrinkListScreen(
viewModel: DrinkListViewModel = viewModel()
) {
val drinks by viewModel.drinks.collectAsState()
LazyColumn {
items(drinks) { drink ->
ListItem(headlineContent = { Text(drink.name) },
supportingContent = { Text("${drink.volume}l · ${drink.price}€") })
}
}
}
@HiltViewModel
class DrinkListViewModel @Inject constructor(
drinkRepository: DrinkRepository,
accountPreferences: AccountPreferences
) : ViewModel() {
private val serverId: Flow<ServerId?> = accountPreferences.state.mapLatest { it.server }
val drinks: StateFlow<List<Drink>> = serverId.flatMapLatest {
it?.let { serverId ->
drinkRepository.getAllFlow(serverId)
} ?: flowOf(emptyList())
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
}
@HiltViewModel
class ServerListViewModel @Inject constructor(
serverRepository: ServerRepository
) : ViewModel() {
val servers: StateFlow<List<Server>> = serverRepository.getAllFlow()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
}
...@@ -34,8 +34,8 @@ import dagger.Provides ...@@ -34,8 +34,8 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import de.chaosdorf.meteroid.AccountPreferences import de.chaosdorf.meteroid.storage.AccountPreferences
import de.chaosdorf.meteroid.AccountPreferencesImpl import de.chaosdorf.meteroid.storage.AccountPreferencesImpl
import javax.inject.Singleton import javax.inject.Singleton
val Context.accountDataStore: DataStore<Preferences> by preferencesDataStore(name = "account") val Context.accountDataStore: DataStore<Preferences> by preferencesDataStore(name = "account")
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid package de.chaosdorf.meteroid.storage
import de.chaosdorf.mete.UserId import de.chaosdorf.mete.UserId
import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.ServerId
...@@ -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(userId: UserId?) suspend fun setUser(user: UserId?)
} }
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid package de.chaosdorf.meteroid.storage
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
...@@ -55,9 +55,9 @@ class AccountPreferencesImpl @Inject constructor( ...@@ -55,9 +55,9 @@ class AccountPreferencesImpl @Inject constructor(
} }
} }
override suspend fun setUser(userId: UserId?) { override suspend fun setUser(user: UserId?) {
dataStore.edit { dataStore.edit {
it[SERVER_KEY] = userId?.value ?: -1L it[USER_KEY] = user?.value ?: -1L
} }
} }
......
...@@ -22,11 +22,12 @@ ...@@ -22,11 +22,12 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid package de.chaosdorf.meteroid.storage
import androidx.room.withTransaction import androidx.room.withTransaction
import de.chaosdorf.mete.DrinkId import de.chaosdorf.mete.DrinkId
import de.chaosdorf.mete.v1.MeteApiV1Factory import de.chaosdorf.mete.v1.MeteApiV1Factory
import de.chaosdorf.meteroid.MeteroidDatabase
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.Server import de.chaosdorf.meteroid.model.Server
......
...@@ -22,8 +22,9 @@ ...@@ -22,8 +22,9 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
package de.chaosdorf.meteroid package de.chaosdorf.meteroid.storage
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
...@@ -31,7 +32,17 @@ abstract class SyncHandler<Context, Entry, Key> { ...@@ -31,7 +32,17 @@ abstract class SyncHandler<Context, Entry, Key> {
sealed class State { sealed class State {
data object Idle : State() data object Idle : State()
data object Loading : State() data object Loading : State()
data class Error(val message: String) : State() data class Error(val message: String = "") : State() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
} }
abstract suspend fun withTransaction(block: suspend () -> Unit) abstract suspend fun withTransaction(block: suspend () -> Unit)
...@@ -48,7 +59,12 @@ abstract class SyncHandler<Context, Entry, Key> { ...@@ -48,7 +59,12 @@ abstract class SyncHandler<Context, Entry, Key> {
val state: StateFlow<State> = _state val state: StateFlow<State> = _state
suspend fun sync(context: Context) { suspend fun sync(context: Context) {
if (_state.compareAndSet(State.Idle, State.Loading)) { if (_state.compareAndSet(State.Idle, State.Loading) || _state.compareAndSet(
State.Error(),
State.Loading
)
) {
Log.w(this::class.simpleName, "Started sync")
try { try {
val loadedEntries = loadCurrent(context) val loadedEntries = loadCurrent(context)
withTransaction { withTransaction {
...@@ -64,9 +80,13 @@ abstract class SyncHandler<Context, Entry, Key> { ...@@ -64,9 +80,13 @@ abstract class SyncHandler<Context, Entry, Key> {
} }
} }
_state.value = State.Idle _state.value = State.Idle
Log.w(this::class.simpleName, "Finished sync")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(this::class.simpleName, "Error while syncing data", e)
_state.value = State.Error("Error while syncing data: $e") _state.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.storage
import androidx.room.withTransaction
import de.chaosdorf.mete.UserId
import de.chaosdorf.mete.v1.MeteApiV1Factory
import de.chaosdorf.meteroid.MeteroidDatabase
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 javax.inject.Inject
class UserSyncHandler @Inject constructor(
private val db: MeteroidDatabase,
private val repository: UserRepository
) : SyncHandler<Server, User, UserSyncHandler.Key>() {
data class Key(
val server: ServerId, val user: UserId
)
override suspend fun withTransaction(block: suspend () -> Unit) =
db.withTransaction(block)
override suspend fun store(entry: User) =
repository.save(entry)
override suspend fun delete(key: Key) =
repository.delete(key.server, key.user)
override fun entryToKey(entry: User) = Key(entry.serverId, entry.userId)
override suspend fun loadStored(context: Server): List<User> =
repository.getAll(context.serverId)
override suspend fun loadCurrent(context: Server): List<User> {
val api = MeteApiV1Factory.newInstance(context.url)
val loadedEntries = api.listUsers()
return loadedEntries.map { User.fromModelV1(context.serverId, 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.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import de.chaosdorf.meteroid.ui.home.DrinkListScreen
import de.chaosdorf.meteroid.ui.home.DrinkListViewModel
import de.chaosdorf.meteroid.ui.home.HomeSections
import de.chaosdorf.meteroid.ui.servers.AddServerScreen
import de.chaosdorf.meteroid.ui.servers.ServerListScreen
import de.chaosdorf.meteroid.ui.users.UserListScreen
import kotlinx.coroutines.launch
@Composable
fun AppRouter(viewModel: AppViewModel = viewModel()) {
val scope = rememberCoroutineScope()
val navController = rememberNavController()
val initState by viewModel.initState.collectAsState()
LaunchedEffect(initState) {
when (initState) {
AppViewModel.InitState.LOADING -> navController.navigate(Routes.Init)
AppViewModel.InitState.CREATE_SERVER -> navController.navigate(Routes.Servers.Add)
AppViewModel.InitState.SELECT_SERVER -> navController.navigate(Routes.Servers.List)
AppViewModel.InitState.SELECT_USER -> navController.navigate(Routes.Users.List)
AppViewModel.InitState.HOME -> navController.navigate(Routes.Home.Root)
}
}
NavHost(navController, startDestination = Routes.Init) {
composable(route = Routes.Init) { _ ->
Box {
Text("Loading")
}
}
navigation(route = Routes.Servers.Root, startDestination = Routes.Servers.List) {
composable(Routes.Servers.List) { _ ->
ServerListScreen(
hiltViewModel(),
onAdd = { navController.navigate(Routes.Servers.Add) },
onSelect = {
scope.launch {
viewModel.selectServer(it)
navController.navigate(Routes.Users.List)
}
}
)
}
composable(Routes.Servers.Add) { _ ->
AddServerScreen(
hiltViewModel(),
onAdd = { navController.navigate(Routes.Servers.List) }
)
}
}
navigation(route = Routes.Users.Root, startDestination = Routes.Users.List) {
composable(Routes.Users.List) { _ ->
UserListScreen(
hiltViewModel(),
onAdd = { TODO() },
onSelect = {
scope.launch {
viewModel.selectUser(it)
navController.navigate(Routes.Home.Root)
}
},
onBack = { navController.navigate(Routes.Servers.Root) },
)
}
/*
composable(Routes.Users.Add) { _ ->
AddUserScreen(
hiltViewModel(),
onAdd = { navController.navigate(Routes.Users.List) }
)
}
*/
}
navigation(route = Routes.Home.Root, startDestination = Routes.Home.Purchase) {
composable(Routes.Home.Purchase) { _ ->
val drinkListViewModel = hiltViewModel<DrinkListViewModel>()
MeteroidScaffold(
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) { _ ->
MeteroidScaffold(
routes = HomeSections.entries,
currentRoute = HomeSections.DEPOSIT,
navigateTo = navController::navigate,
onBack = { navController.navigate(Routes.Users.Root) },
) { paddingValues ->
Box(Modifier.padding(paddingValues)) {
Text("TODO: Deposit")
}
}
}
}
}
}
/*
* 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.UserId
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.storage.AccountPreferences
import de.chaosdorf.meteroid.storage.DrinkSyncHandler
import de.chaosdorf.meteroid.storage.UserSyncHandler
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class AppViewModel @Inject constructor(
private val accountPreferences: AccountPreferences,
private val serverRepository: ServerRepository,
private val userSyncHandler: UserSyncHandler,
private val drinkSyncHandler: DrinkSyncHandler,
) : ViewModel() {
val initState: StateFlow<InitState> = accountPreferences.state
.flatMapLatest { preferences ->
serverRepository.getAllFlow()
.mapLatest { it.map(Server::serverId) }
.mapLatest { servers ->
if (servers.isEmpty()) InitState.CREATE_SERVER
else if (!servers.contains(preferences.server)) InitState.SELECT_SERVER
else if (preferences.user == null) InitState.SELECT_USER
else InitState.HOME
}
}.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 {
server.onEach { server ->
if (server != null) {
userSyncHandler.sync(server)
drinkSyncHandler.sync(server)
}
}.launchIn(viewModelScope)
}
suspend fun selectServer(server: ServerId) {
accountPreferences.setServer(server)
accountPreferences.setUser(null)
}
suspend fun selectUser(user: UserId) {
accountPreferences.setUser(user)
}
enum class InitState {
LOADING,
CREATE_SERVER,
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.compose.foundation.layout.RowScope
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
interface MeteroidNavSection {
val title: String
val icon: ImageVector
val route: String
}
@Composable
fun <T : MeteroidNavSection> RowScope.MeteroidNavSections(
routes: Iterable<T>,
currentRoute: T,
navigateTo: (String) -> Unit,
modifier: Modifier = Modifier
) {
for (route in routes) {
NavigationBarItem(
icon = { Icon(route.icon, contentDescription = route.title) },
label = { Text(route.title) },
selected = route == currentRoute,
onClick = { navigateTo(route.route) },
modifier = modifier
)
}
}
/*
* 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.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
@Composable
fun <T : MeteroidNavSection> MeteroidScaffold(
routes: Iterable<T>,
currentRoute: T,
navigateTo: (String) -> Unit,
onBack: () -> Unit,
content: @Composable (PaddingValues) -> Unit
) {
Scaffold(
topBar = {
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 = {
NavigationBar {
MeteroidNavSections(routes, currentRoute, navigateTo)
}
},
content = content
)
}
/*
* 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
object Routes {
const val Init = "init"
object Servers {
const val Root = "servers"
const val List = "$Root/list"
const val Add = "$Root/new"
}
object Users {
const val Root = "users"
const val List = "${Root}/list"
//const val Add = "${Root}/new"
}
object Home {
const val Root = "home"
const val Deposit = "home/deposit"
const val Purchase = "home/purchase"
}
}
/*
* 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.home
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem
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.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import de.chaosdorf.meteroid.storage.SyncHandler
@Preview
@Composable
fun DrinkListScreen(
viewModel: DrinkListViewModel = viewModel(),
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
) {
val drinks by viewModel.drinks.collectAsState()
val syncState by viewModel.syncState.collectAsState()
Column(modifier = modifier) {
if (syncState == SyncHandler.State.Loading) {
LinearProgressIndicator()
}
LazyColumn {
items(drinks) { drink ->
ListItem(headlineContent = { Text(drink.name) },
supportingContent = { Text("${drink.volume}l · ${drink.price}€") })
}
}
}
}
/*
* 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.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.meteroid.model.Drink
import de.chaosdorf.meteroid.model.DrinkRepository
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.storage.AccountPreferences
import de.chaosdorf.meteroid.storage.DrinkSyncHandler
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class DrinkListViewModel @Inject constructor(
drinkRepository: DrinkRepository,
accountPreferences: AccountPreferences,
syncHandler: DrinkSyncHandler
) : ViewModel() {
private val serverId: Flow<ServerId?> = accountPreferences.state.mapLatest { it.server }
val drinks: StateFlow<List<Drink>> = serverId.flatMapLatest {
it?.let { serverId ->
drinkRepository.getAllFlow(serverId)
} ?: flowOf(emptyList())
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val syncState = 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.home
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Money
import androidx.compose.ui.graphics.vector.ImageVector
import de.chaosdorf.meteroid.icons.MeteroidIcons
import de.chaosdorf.meteroid.icons.twotone.WaterFull
import de.chaosdorf.meteroid.ui.MeteroidNavSection
import de.chaosdorf.meteroid.ui.Routes
enum class HomeSections(
override val title: String,
override val icon: ImageVector,
override val route: String
) : MeteroidNavSection {
PURCHASE("Drinks", MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase),
DEPOSIT("Money", Icons.TwoTone.Money, Routes.Home.Deposit);
}
/*
* 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.servers
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import kotlinx.coroutines.launch
@Composable
fun AddServerScreen(
viewModel: AddServerViewModel = viewModel(),
onAdd: () -> Unit = {}
) {
val scope = rememberCoroutineScope()
val url by viewModel.url.collectAsState()
val server by viewModel.server.collectAsState()
Column {
TextField(
label = { Text("Server URL") },
value = url,
onValueChange = { viewModel.url.value = it }
)
Button(onClick = {
scope.launch {
viewModel.addServer()
onAdd()
}
}) {
Text("Add Server")
}
server?.let { server ->
Text(server.url)
Text(server.name ?: "null1")
AsyncImage(model = server.logoUrl, contentDescription = null)
}
}
}
/*
* 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.servers
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.mete.v1.MeteApiV1Factory
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerId
import de.chaosdorf.meteroid.model.ServerRepository
import de.chaosdorf.meteroid.util.findBestIcon
import de.chaosdorf.meteroid.util.resolve
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@HiltViewModel
class AddServerViewModel @Inject constructor(
private val repository: ServerRepository
) : ViewModel() {
val url = MutableStateFlow("")
private suspend fun buildServer(
id: ServerId,
url: String
): Server? = try {
val api = MeteApiV1Factory.newInstance(url)
val manifest = api.getManifest()
val icon = manifest?.findBestIcon()
Server(
id,
manifest?.name,
url,
icon?.resolve(url)
)
} catch (_: Exception) {
null
}
val server: StateFlow<Server?> = url
.debounce(300.milliseconds)
.mapLatest { buildServer(ServerId(-1), it) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
suspend fun addServer() {
val highestServerId = repository.getAll().maxOfOrNull { it.serverId.value } ?: 0
val serverId = ServerId(highestServerId + 1)
val server = buildServer(serverId, url.value)
if (server != null) {
repository.save(server)
}
}
}
/*
* 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.servers
import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ListItem
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.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import de.chaosdorf.meteroid.model.ServerId
@Preview
@Composable
fun ServerListScreen(
viewModel: ServerListViewModel = viewModel(),
onAdd: () -> Unit = {},
onSelect: (ServerId) -> Unit = {}
) {
val servers by viewModel.servers.collectAsState()
LazyColumn {
items(servers) { server ->
ListItem(
headlineContent = { Text(server.name ?: server.url) },
modifier = Modifier.clickable { onSelect(server.serverId) }
)
}
item {
ListItem(
headlineContent = { Text("Add Server") },
modifier = Modifier.clickable { onAdd() }
)
}
}
}
/*
* 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.servers
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.chaosdorf.meteroid.model.Server
import de.chaosdorf.meteroid.model.ServerRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class ServerListViewModel @Inject constructor(
serverRepository: ServerRepository
) : ViewModel() {
val servers: StateFlow<List<Server>> = serverRepository.getAllFlow()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment