diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fc70942eb38278e03007515aaf21a89d517e1593..25cde5cee1030f3f9ef1cb9ee3850b635e880e36 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -68,6 +68,10 @@ dependencies { testImplementation(libs.junit.params) testRuntimeOnly(libs.junit.engine) + implementation(platform(libs.androidx.compose.bom)) + testImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat.resources) @@ -75,9 +79,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.splashscreen) - implementation(libs.androidx.compose.animation) - implementation(libs.androidx.compose.compiler) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material) implementation(libs.androidx.compose.material.icons) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index 15ee94a0c23d5a5a991b1216653c3aaf8d0e989b..aa51d658aab0a3a345533cc16b43bc3e80accf59 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt @@ -28,184 +28,31 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.core.snap -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation3.runtime.* -import androidx.navigation3.ui.NavDisplay -import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.withCreationCallback -import de.chaosdorf.meteroid.theme.MeteroidTheme -import de.chaosdorf.meteroid.ui.* -import de.chaosdorf.meteroid.ui.common.BottomBar -import de.chaosdorf.meteroid.ui.navigation.TopNavigation -import de.chaosdorf.meteroid.ui.purchase.PurchaseRoute -import de.chaosdorf.meteroid.viewmodel.NavigationViewModel -import de.chaosdorf.meteroid.viewmodel.* -import kotlinx.coroutines.flow.update +import de.chaosdorf.meteroid.viewmodel.InitViewModel +import de.chaosdorf.meteroid.viewmodel.InitViewModelFactory @AndroidEntryPoint class MainActivity : ComponentActivity() { + val initViewModel by viewModels<InitViewModel>( + extrasProducer = { + defaultViewModelCreationExtras.withCreationCallback<InitViewModelFactory> { factory -> + factory.create() + } + } + ) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val viewModel by viewModels<InitViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<InitViewModelFactory> { factory -> - factory.create() - } - } - ) - installSplashScreen().setKeepOnScreenCondition { - viewModel.initialRoute.value == null + initViewModel.initialRoute.value == null } setContent { - val initialRoute = viewModel.initialRoute.collectAsState().value - - MeteroidTheme { - if (initialRoute != null) { - val navigationViewModel by viewModels<NavigationViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<NavigationViewModelFactory> { factory -> - factory.create(initialRoute) - } - } - ) - - val backStack by navigationViewModel.backStack.collectAsState() - - Scaffold( - topBar = { TopNavigation(navigationViewModel) }, - bottomBar = { BottomBar(navigationViewModel) } - ) { paddingValues -> - NavDisplay( - backStack = backStack, - onBack = { count -> - navigationViewModel.backStack.update { - it.dropLast(count) - } - }, - entryDecorators = listOf( - rememberSceneSetupNavEntryDecorator(), - rememberSavedStateNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator(), - ), - // BEGIN WORKAROUND - // TODO FIXED IN navigation3 1.0.0-alpha04 Jun 18 - transitionSpec = { - ContentTransform( - fadeIn(animationSpec = snap()), - fadeOut(animationSpec = snap()) - ) - }, - popTransitionSpec = { - ContentTransform( - fadeIn(animationSpec = snap()), - fadeOut(animationSpec = snap()) - ) - }, - predictivePopTransitionSpec = { - ContentTransform( - fadeIn(animationSpec = snap()), - fadeOut(animationSpec = snap()) - ) - }, - // END - entryProvider = entryProvider { - entry<MeteroidRoute.Setup> { - val viewModel by viewModels<SetupViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<SetupViewModelFactory> { factory -> - factory.create() - } - } - ) - SetupRoute(viewModel, navigationViewModel, paddingValues) - } - entry<MeteroidRoute.Settings> { - val viewModel by viewModels<SettingsViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<SettingsViewModelFactory> { factory -> - factory.create() - } - } - ) - SettingsRoute(viewModel, navigationViewModel, paddingValues) - } - entry<MeteroidRoute.ServerList> { - val viewModel by viewModels<ServerListViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<ServerListViewModelFactory> { factory -> - factory.create() - } - } - ) - ServerListRoute(viewModel, navigationViewModel, paddingValues) - } - entry<MeteroidRoute.UserList> { - val viewModel by viewModels<UserListViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<UserListViewModelFactory> { factory -> - factory.create(it.serverId.value) - } - } - ) - UserListRoute(viewModel, navigationViewModel, paddingValues) - } - entry<MeteroidRoute.Purchase> { - val viewModel by viewModels<PurchaseViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<PurchaseViewModelFactory> { factory -> - factory.create(it.serverId.value, it.userId.value) - } - } - ) - PurchaseRoute(viewModel, navigationViewModel, paddingValues) - } - entry<MeteroidRoute.Deposit> { - val viewModel by viewModels<DepositViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<DepositViewModelFactory> { factory -> - factory.create(it.serverId.value, it.userId.value) - } - } - ) - DepositRoute(viewModel, navigationViewModel, paddingValues) - } - entry<MeteroidRoute.History> { - val viewModel by viewModels<HistoryViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<HistoryViewModelFactory> { factory -> - factory.create(it.serverId.value, it.userId.value) - } - } - ) - HistoryRoute(viewModel, navigationViewModel, paddingValues) - } - entry<MeteroidRoute.Wrapped> { - val viewModel by viewModels<WrappedViewModel>( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback<WrappedViewModelFactory> { factory -> - factory.create(it.serverId.value, it.userId.value) - } - } - ) - WrappedRoute(viewModel, navigationViewModel, paddingValues) - } - } - ) - } - } - } + MeteroidApp(initViewModel) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidApp.kt similarity index 50% rename from app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackExtensions.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidApp.kt index f96bb7a7e83811ac655bade058e52046b0707652..d516e1dbc2e139d1f53f79c4e08d13c3dec555fa 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackExtensions.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidApp.kt @@ -22,34 +22,37 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.util +package de.chaosdorf.meteroid -import android.util.Log -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList -import androidx.navigation3.runtime.NavBackStack -import androidx.navigation3.runtime.NavKey -import de.chaosdorf.meteroid.ui.MeteroidRoute +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.hilt.navigation.compose.hiltViewModel +import de.chaosdorf.meteroid.theme.MeteroidTheme +import de.chaosdorf.meteroid.ui.common.BottomBar +import de.chaosdorf.meteroid.ui.navigation.TopNavigation +import de.chaosdorf.meteroid.viewmodel.InitViewModel +import de.chaosdorf.meteroid.viewmodel.NavigationViewModel +import de.chaosdorf.meteroid.viewmodel.NavigationViewModelFactory -@PublishedApi -internal fun <T> List<T>.toSnapshotStateList(): SnapshotStateList<T> = - SnapshotStateList<T>().also { it.addAll(this) } +@Composable +fun MeteroidApp( + initViewModel: InitViewModel, +) { + val initialRoute = initViewModel.initialRoute.collectAsState().value -inline fun NavBackStack.update(vararg elements: NavKey) { - Log.i("Navigator", "Updating backstack: ${this.toList()} -> ${elements.toList()}") - require(elements.isNotEmpty()) { - "Error: backstack cannot be empty" - } - prependStateRecord(mutableStateListOf(*elements).firstStateRecord) -} + MeteroidTheme { + if (initialRoute != null) { + val navigationViewModel = hiltViewModel<NavigationViewModel, NavigationViewModelFactory> { factory -> + factory.create(initialRoute) + } -inline fun NavBackStack.update(elements: List<NavKey>) { - Log.i("Navigator", "Updating backstack: ${this.toList()} -> $elements") - require(elements.isNotEmpty()) { - "Error: backstack cannot be empty" + Scaffold( + topBar = { TopNavigation(navigationViewModel) }, + bottomBar = { BottomBar(navigationViewModel) }, + ) { paddingValues -> + MeteroidRouter(navigationViewModel, paddingValues) + } + } } - prependStateRecord(elements.toSnapshotStateList().firstStateRecord) } - -inline fun NavBackStack.update(crossinline f: (List<NavKey>) -> List<NavKey>) = - update(f(this)) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidRouter.kt new file mode 100644 index 0000000000000000000000000000000000000000..f4890d2174d24954689ce44f558e53ddca4bd6ad --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidRouter.kt @@ -0,0 +1,138 @@ +/* + * 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 + +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.snap +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator +import androidx.navigation3.runtime.entry +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator +import de.chaosdorf.meteroid.ui.* +import de.chaosdorf.meteroid.ui.purchase.PurchaseRoute +import de.chaosdorf.meteroid.viewmodel.* +import kotlinx.coroutines.flow.update + +@Composable +fun MeteroidRouter( + navigator: Navigator, + contentPadding: PaddingValues, +) { + val backStack by navigator.backStack.collectAsState() + + NavDisplay( + backStack = backStack, + onBack = { count -> + navigator.backStack.update { + it.dropLast(count) + } + }, + entryDecorators = listOf( + rememberSceneSetupNavEntryDecorator(), + rememberSavedStateNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + // BEGIN WORKAROUND + // TODO FIXED IN navigation3 1.0.0-alpha04 Jun 18 + transitionSpec = { + ContentTransform( + fadeIn(animationSpec = snap()), + fadeOut(animationSpec = snap()) + ) + }, + popTransitionSpec = { + ContentTransform( + fadeIn(animationSpec = snap()), + fadeOut(animationSpec = snap()) + ) + }, + predictivePopTransitionSpec = { + ContentTransform( + fadeIn(animationSpec = snap()), + fadeOut(animationSpec = snap()) + ) + }, + // END + entryProvider = entryProvider { + entry<MeteroidRoute.Setup> { + val viewModel = hiltViewModel<SetupViewModel, SetupViewModelFactory> { factory -> + factory.create() + } + SetupRoute(viewModel, navigator, contentPadding) + } + entry<MeteroidRoute.Settings> { + val viewModel = hiltViewModel<SettingsViewModel, SettingsViewModelFactory> { factory -> + factory.create() + } + SettingsRoute(viewModel, navigator, contentPadding) + } + entry<MeteroidRoute.ServerList> { + val viewModel = hiltViewModel<ServerListViewModel, ServerListViewModelFactory> { factory -> + factory.create() + } + ServerListRoute(viewModel, navigator, contentPadding) + } + entry<MeteroidRoute.UserList> { + val viewModel = hiltViewModel<UserListViewModel, UserListViewModelFactory> { factory -> + factory.create(it.serverId.value) + } + UserListRoute(viewModel, navigator, contentPadding) + } + entry<MeteroidRoute.Purchase> { + val viewModel = hiltViewModel<PurchaseViewModel, PurchaseViewModelFactory> { factory -> + factory.create(it.serverId.value, it.userId.value) + } + PurchaseRoute(viewModel, navigator, contentPadding) + } + entry<MeteroidRoute.Deposit> { + val viewModel = hiltViewModel<DepositViewModel, DepositViewModelFactory> { factory -> + factory.create(it.serverId.value, it.userId.value) + } + DepositRoute(viewModel, navigator, contentPadding) + } + entry<MeteroidRoute.History> { + val viewModel = hiltViewModel<HistoryViewModel, HistoryViewModelFactory> { factory -> + factory.create(it.serverId.value, it.userId.value) + } + HistoryRoute(viewModel, navigator, contentPadding) + } + entry<MeteroidRoute.Wrapped> { + val viewModel = hiltViewModel<WrappedViewModel, WrappedViewModelFactory> { factory -> + factory.create(it.serverId.value, it.userId.value) + } + WrappedRoute(viewModel, navigator, contentPadding) + } + } + ) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt index 359efb124fc894b144c77e0807f02f90ead7c4b3..86fa2dba8eea79e6244a80e54989ccec0ad4bf8a 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt @@ -51,7 +51,7 @@ class SyncManager @Inject constructor( } suspend fun checkOffline(server: Server): Boolean { - val updated = factory.newServer(server.serverId, server.url) + val updated = factory.newServer(server.url) return if (updated == null) { true } else { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SetupRoute.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SetupRoute.kt index 59334ef3f41a54c49a3f0060181df75dfd4c723f..de2b53f99f09fe743e444c794e5b140b3d85a367 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SetupRoute.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/SetupRoute.kt @@ -31,18 +31,16 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage +import de.chaosdorf.meteroid.ui.common.ErrorBanner import de.chaosdorf.meteroid.viewmodel.Navigator import de.chaosdorf.meteroid.viewmodel.SetupViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import okhttp3.HttpUrl.Companion.toHttpUrl @Composable @@ -51,7 +49,6 @@ fun SetupRoute( navigator: Navigator, contentPadding: PaddingValues, ) { - val coroutineScope = rememberCoroutineScope() val serverUrl by viewModel.serverUrl.collectAsState() val server by viewModel.server.collectAsState() val error by viewModel.error.collectAsState() @@ -59,9 +56,10 @@ fun SetupRoute( Column( Modifier .padding(contentPadding) - .padding(16.dp, 8.dp) + .padding(16.dp, 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - TextField( + OutlinedTextField( label = { Text("Server URL") }, value = serverUrl, onValueChange = { viewModel.serverUrl.value = it }, @@ -69,18 +67,11 @@ fun SetupRoute( ) error?.let { - Surface( - color = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ) { - Text(it) - } + ErrorBanner(it) } server?.let { server -> - Card( - modifier = Modifier.padding(vertical = 8.dp) - ) { + Card { Row(modifier = Modifier.padding(16.dp, 8.dp)) { AsyncImage( server.logoUrl, @@ -110,17 +101,16 @@ fun SetupRoute( ) IconButton(onClick = { - coroutineScope.launch(Dispatchers.IO) { - val server = viewModel.add() - if (server != null) { + viewModel.add( + onSuccess = { new -> navigator.backStack.update { listOf( MeteroidRoute.ServerList, - MeteroidRoute.UserList(server.serverId) + MeteroidRoute.UserList(new.serverId) ) } } - } + ) }) { Icon(Icons.Default.Add, contentDescription = "Add Server") } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/ErrorBanner.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/ErrorBanner.kt new file mode 100644 index 0000000000000000000000000000000000000000..427f86f75d76ec480ef9e96a2d3d68eb119f69bf --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/common/ErrorBanner.kt @@ -0,0 +1,71 @@ +/* + * 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.ui.common + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview(showBackground = true) +@Composable +fun ErrorBanner( + title: String = "An error has occurred", + content: @Composable () -> Unit = {}, +) { + Surface( + color = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + shape = RoundedCornerShape(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Companion.CenterVertically, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .heightIn(32.dp), + ) { + Column(verticalArrangement = Arrangement.Center) { + Text( + title, + style = MaterialTheme.typography.bodyLarge, + ) + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodyMedium, + content = content, + ) + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt index 244d2a2979ec2adf643b50576f0290723842ffcf..89d2476ea9d492a2d1da9e09257e28dd929aa19d 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt @@ -33,7 +33,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/PersistentNavigation.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/PersistentNavigation.kt index 78d873e889ca448ed417254193a8038edd3c501d..71de8577122e270a072c0b5e60e554030d6e5809 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/PersistentNavigation.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/PersistentNavigation.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.TileMode import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.unit.dp import de.chaosdorf.meteroid.viewmodel.NavigationElement import de.chaosdorf.meteroid.viewmodel.NavigationViewModel diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt index e8371e2e2fc77e5ba6a5d36ab288eebaa6d83e2e..1e2778a332784b1c8ec9745af051ea329f4c4451 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt @@ -29,7 +29,7 @@ import de.chaosdorf.mete.model.ServerId import de.chaosdorf.meteroid.model.Server import java.net.URI -suspend fun MeteApiFactory.newServer(serverId: ServerId, baseUrl: String): Server? = try { +suspend fun MeteApiFactory.newServer(baseUrl: String): Server? = try { val api = newInstance(baseUrl) val manifest = api.getManifest() @@ -43,10 +43,10 @@ suspend fun MeteApiFactory.newServer(serverId: ServerId, baseUrl: String): Serve val iconUrl = icon?.src?.let { URI(baseUrl).resolve(it).toString() } Server( - serverId, - manifest?.name, - baseUrl, - iconUrl + serverId = ServerId(0), + name = manifest?.name, + url = baseUrl, + logoUrl = iconUrl ) } catch (_: Exception) { null diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/NavigationViewModel.kt index d1184adb7a145c3f9faeb6c8a6a3a1208a369fe0..09a8b342c794ad3ac6293177e3433b9c9ab159e9 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/NavigationViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/NavigationViewModel.kt @@ -24,7 +24,6 @@ package de.chaosdorf.meteroid.viewmodel -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation3.runtime.NavKey @@ -32,8 +31,6 @@ 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.model.* import de.chaosdorf.meteroid.storage.AccountPreferences import de.chaosdorf.meteroid.sync.AccountProvider diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SetupViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SetupViewModel.kt index f232d623c872b81b9fe407631ad2a6028193ea2f..eb1a5e7105b33834c5f5fb018dae51c2401e6173 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SetupViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/viewmodel/SetupViewModel.kt @@ -24,6 +24,7 @@ package de.chaosdorf.meteroid.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.assisted.AssistedFactory @@ -35,14 +36,11 @@ 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.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview -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 kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.milliseconds @AssistedFactory @@ -59,25 +57,32 @@ class SetupViewModel @AssistedInject constructor( ) : ViewModel() { val serverUrl = MutableStateFlow("") - val server: StateFlow<Server?> = serverUrl + private val baseUrl = serverUrl .debounce(300.milliseconds) - .mapLatest { apiFactory.newServer(ServerId(0), it) } + .mapLatest { if (it.startsWith("http://") || it.startsWith("https://")) it else "http://$it" } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), "") + + val server: StateFlow<Server?> = baseUrl + .mapLatest { apiFactory.newServer(it) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) val error = MutableStateFlow<String?>(null) - suspend fun add(): Server? { - val highestServerId = serverRepository.getAll().maxOfOrNull { it.serverId.value } ?: 0 - val serverId = ServerId(highestServerId + 1) - val server = apiFactory.newServer(serverId, serverUrl.value) + fun add(onSuccess: (Server) -> Unit = {}) { + viewModelScope.launch(Dispatchers.IO) { + val server = apiFactory.newServer(baseUrl.value) if (server != null) { - serverRepository.save(server) - syncManager.sync(server, null, incremental = false) + Log.i("Setup", "Saving server $server") + val id = serverRepository.save(server) + val persistedServer = server.copy(serverId = ServerId(id)) + Log.i("Setup", "Syncing server $persistedServer") + syncManager.sync(persistedServer, null, incremental = false) error.value = null - return server + Log.i("Setup", "Calling success handler") + onSuccess(persistedServer) } else { error.value = "Server not found" - return null } + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9e66caa3235bc69647cb94df8bba2181477bf67..b34bb1dc66d03e589e5762307d41fe5f0ba7a3b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,15 +8,13 @@ kotlinxSerializationJson = "1.8.1" androidGradlePlugin = "8.10.1" androidx-activity = "1.10.1" androidx-appcompat = "1.7.1" -androidx-compose = "1.8.2" +androidx-compose-bom = "2025.06.00" androidx-compose-compiler = "1.5.15" androidx-datastore = "1.1.7" 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" -androidx-material-icons = "1.7.8" dagger-hilt = "2.56.2" androidx-hilt = "1.2.0" @@ -34,15 +32,15 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-appcompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "androidx-appcompat" } -androidx-compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "androidx-compose-compiler" } -androidx-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "androidx-compose" } -androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" } -androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-material-icons" } -androidx-compose-material = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-material3" } -androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidx-compose" } -androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test", version.ref = "androidx-compose" } -androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose" } -androidx-compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-compose-material = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } +androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" } diff --git a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json index 92efcbbdb205fafdea4788b4f830b585852e13a6..bea7c6864eadebf58c2236c0937dd7f94325d86b 100644 --- a/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json +++ b/persistence/room/schemas/de.chaosdorf.meteroid.MeteroidDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "e26316f758271c58bc953e756fc16e7d", + "identityHash": "4783167a5b20f9424d766a24e80f54a5", "entities": [ { "tableName": "Drink", @@ -41,8 +41,7 @@ { "fieldPath": "caffeine", "columnName": "caffeine", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "price", @@ -53,8 +52,7 @@ { "fieldPath": "logoUrl", "columnName": "logoUrl", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { @@ -64,7 +62,6 @@ "drinkId" ] }, - "indices": [], "foreignKeys": [ { "table": "Server", @@ -81,7 +78,7 @@ }, { "tableName": "Server", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT, PRIMARY KEY(`serverId`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `logoUrl` TEXT)", "fields": [ { "fieldPath": "serverId", @@ -92,8 +89,7 @@ { "fieldPath": "name", "columnName": "name", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "url", @@ -104,18 +100,15 @@ { "fieldPath": "logoUrl", "columnName": "logoUrl", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { - "autoGenerate": false, + "autoGenerate": true, "columnNames": [ "serverId" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "User", @@ -148,8 +141,7 @@ { "fieldPath": "email", "columnName": "email", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "balance", @@ -177,7 +169,6 @@ "userId" ] }, - "indices": [], "foreignKeys": [ { "table": "Server", @@ -216,7 +207,6 @@ "userId" ] }, - "indices": [], "foreignKeys": [ { "table": "Server", @@ -269,8 +259,7 @@ { "fieldPath": "drinkId", "columnName": "drinkId", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "difference", @@ -355,10 +344,9 @@ ] } ], - "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e26316f758271c58bc953e756fc16e7d')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4783167a5b20f9424d766a24e80f54a5')" ] } } \ No newline at end of file diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt index 1c217938eb203d9ad67107b44b36872b89e01de4..d12d18bd54cc7e4b221e7f096f31ff4e53ed6b2c 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.Flow @Entity data class Server( - @PrimaryKey + @PrimaryKey(autoGenerate = true) val serverId: ServerId, val name: String?, val url: String, @@ -58,7 +58,7 @@ interface ServerRepository { fun getAllFlow(): Flow<List<Server>> @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun save(server: Server) + suspend fun save(server: Server): Long @Query("DELETE FROM Server WHERE serverId = :id") suspend fun delete(id: ServerId)