From db1d2d74522d911d387e8c02d302085027755891 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <mail@justjanne.de> Date: Sun, 3 Dec 2023 22:03:33 +0100 Subject: [PATCH] feat: redesigned navigation --- .editorconfig | 8 +- README.md | 6 + .../de/chaosdorf/mete/model/BarcodeId.kt | 1 + .../kotlin/de/chaosdorf/mete/model/DrinkId.kt | 1 + .../kotlin/de/chaosdorf/mete/model/MeteApi.kt | 3 - .../de/chaosdorf/mete/model/ServerId.kt | 31 +++ .../de/chaosdorf/mete/model/TransactionId.kt | 1 + .../kotlin/de/chaosdorf/mete/model/UserId.kt | 1 + .../kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt | 11 +- app/src/main/AndroidManifest.xml | 9 +- .../de/chaosdorf/meteroid/MainActivity.kt | 17 +- ...rListViewModel.kt => MeteroidViewModel.kt} | 28 ++- .../chaosdorf/meteroid/di/DatabaseModule.kt | 6 +- .../meteroid/storage/AccountPreferences.kt | 2 +- .../storage/AccountPreferencesImpl.kt | 3 +- .../meteroid/sync/AccountProvider.kt | 11 +- .../meteroid/sync/DrinkSyncHandler.kt | 4 +- .../de/chaosdorf/meteroid/sync/SyncManager.kt | 24 +- .../meteroid/sync/TransactionSyncHandler.kt | 9 +- .../meteroid/sync/UserSyncHandler.kt | 4 +- .../sync/base/BaseIncrementalSyncHandler.kt | 8 +- .../meteroid/sync/base/BaseSyncHandler.kt | 33 +-- .../meteroid/sync/base/SyncHandler.kt | 16 ++ .../meteroid/{ui => }/theme/Color.kt | 26 ++- .../meteroid/{ui => }/theme/Theme.kt | 11 +- .../meteroid/{ui => }/theme/ThemeGradient.kt | 8 +- .../chaosdorf/meteroid/{ui => }/theme/Type.kt | 2 +- .../{ => theme}/icons/MeteroidIcons.kt | 2 +- .../{ => theme}/icons/filled/WaterFull.kt | 34 ++- .../{ => theme}/icons/outlined/WaterFull.kt | 6 +- .../{ => theme}/icons/twotone/WaterFull.kt | 6 +- .../de/chaosdorf/meteroid/ui/AppRouter.kt | 192 --------------- .../de/chaosdorf/meteroid/ui/AppViewModel.kt | 97 -------- .../kotlin/de/chaosdorf/meteroid/ui/Avatar.kt | 114 +++++++++ .../chaosdorf/meteroid/ui/MeteroidRouter.kt | 173 ++++++++++++++ .../chaosdorf/meteroid/ui/MeteroidScaffold.kt | 51 ++++ .../chaosdorf/meteroid/ui/MeteroidScreen.kt | 118 ++++++++++ .../meteroid/ui/NavigationViewModel.kt | 97 -------- .../meteroid/ui/home/MeteroidBottomBar.kt | 130 +++++++++++ .../meteroid/ui/home/MeteroidBottomBarItem.kt | 54 +++++ .../meteroid/ui/{ => home}/PriceBadge.kt | 3 +- .../deposit/DepositMoneyItem.kt} | 12 +- .../deposit/DepositScreen.kt} | 10 +- .../deposit/DepositViewModel.kt} | 19 +- .../ui/home/deposit/MonetaryAmount.kt | 39 ++++ .../purchase/PurchaseDrinkTile.kt} | 61 ++--- .../purchase/PurchaseFilterChip.kt} | 10 +- .../purchase/PurchaseScreen.kt} | 30 ++- .../purchase/PurchaseViewModel.kt} | 14 +- .../TransactionHistoryItem.kt} | 14 +- .../TransactionHistoryScreen.kt} | 10 +- .../TransactionHistoryViewModel.kt} | 12 +- .../transactionhistory}/TransactionInfo.kt | 6 +- .../ui/{ => home}/wrapped/WrappedScreen.kt | 16 +- .../ui/{ => home}/wrapped/WrappedSlide.kt | 28 +-- .../ui/{ => home}/wrapped/WrappedViewModel.kt | 11 +- .../ui/navigation/MeteroidBottomBar.kt | 101 -------- .../ui/navigation/MeteroidNavigation.kt | 171 ++++++++++++++ .../meteroid/ui/navigation/MeteroidTopBar.kt | 220 ------------------ ...Sections.kt => NavigationAddServerItem.kt} | 47 ++-- ...ion.kt => NavigationAnimationContainer.kt} | 23 +- .../ui/navigation/NavigationElement.kt | 73 ++++++ .../meteroid/ui/navigation/NavigationScrim.kt | 51 ++++ .../ui/navigation/NavigationServerItem.kt | 55 +++++ .../ui/navigation/NavigationSettingsItem.kt | 56 +++++ .../ui/navigation/NavigationUserItem.kt | 80 +++++++ .../ui/navigation/NavigationUserListItem.kt | 55 +++++ .../ui/navigation/NavigationViewModel.kt | 120 ++++++++++ .../meteroid/ui/navigation/Routes.kt | 58 ----- .../meteroid/ui/servers/AddServerScreen.kt | 19 +- .../meteroid/ui/servers/AddServerViewModel.kt | 9 +- .../meteroid/ui/servers/ServerListScreen.kt | 81 ------- .../meteroid/ui/settings/SettingsScreen.kt | 45 ++++ .../settings/SettingsViewModel.kt} | 7 +- .../ui/{users => userlist}/UserListItem.kt | 4 +- .../ui/{users => userlist}/UserListScreen.kt | 25 +- .../{users => userlist}/UserListViewModel.kt | 4 +- .../chaosdorf/meteroid/ui/users/UserTile.kt | 102 -------- .../meteroid/util/BundleExtensions.kt | 30 +++ .../HumanReadableHost.kt} | 36 +-- .../meteroid/util/MeteApiFactoryExtensions.kt | 2 +- .../util/NavBackStackEntryExtensions.kt | 42 ++++ .../meteroid/util/NavGraphExtensions.kt | 35 +++ .../meteroid/util/RememberAvatarPainter.kt | 18 +- app/src/main/res/drawable/ic_launcher.xml | 29 +-- .../res/drawable/ic_launcher_foreground.xml | 27 +-- app/src/main/res/drawable/ic_splash.xml | 25 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 +- .../res/values/ic_launcher_background.xml | 4 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- .../kotlin/AndroidApplicationConvention.kt | 2 +- .../src/main/kotlin/KotlinConvention.kt | 3 - .../src/main/kotlin/util/ProjectExtensions.kt | 2 +- gradle/libs.versions.toml | 29 ++- lint.xml | 28 +-- .../de/chaosdorf/meteroid/MeteroidDatabase.kt | 12 +- .../de/chaosdorf/meteroid/model/Drink.kt | 8 +- .../de/chaosdorf/meteroid/model/PinnedUser.kt | 8 +- .../de/chaosdorf/meteroid/model/Server.kt | 12 +- .../chaosdorf/meteroid/model/Transaction.kt | 14 +- .../de/chaosdorf/meteroid/model/User.kt | 28 +-- .../de/chaosdorf/meteroid/util/GravatarUrl.kt | 60 +++++ .../util/KotlinDatetimeTypeConverter.kt | 1 + 105 files changed, 2046 insertions(+), 1484 deletions(-) create mode 100644 api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt rename app/src/main/kotlin/de/chaosdorf/meteroid/{ui/servers/ServerListViewModel.kt => MeteroidViewModel.kt} (72%) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ui => }/theme/Color.kt (74%) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ui => }/theme/Theme.kt (93%) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ui => }/theme/ThemeGradient.kt (92%) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ui => }/theme/Type.kt (95%) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ => theme}/icons/MeteroidIcons.kt (96%) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ => theme}/icons/filled/WaterFull.kt (58%) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ => theme}/icons/outlined/WaterFull.kt (95%) rename app/src/main/kotlin/de/chaosdorf/meteroid/{ => theme}/icons/twotone/WaterFull.kt (96%) delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{ => home}/PriceBadge.kt (97%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{money/MoneyTile.kt => home/deposit/DepositMoneyItem.kt} (85%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{money/MoneyListScreen.kt => home/deposit/DepositScreen.kt} (91%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{money/MoneyListViewModel.kt => home/deposit/DepositViewModel.kt} (75%) create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{drinks/DrinkTile.kt => home/purchase/PurchaseDrinkTile.kt} (74%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{drinks/DrinkListFilterChip.kt => home/purchase/PurchaseFilterChip.kt} (87%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{drinks/DrinkListScreen.kt => home/purchase/PurchaseScreen.kt} (72%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{drinks/DrinkListViewModel.kt => home/purchase/PurchaseViewModel.kt} (89%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{transactions/PurchaseListItem.kt => home/transactionhistory/TransactionHistoryItem.kt} (90%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{transactions/PurchaseListScreen.kt => home/transactionhistory/TransactionHistoryScreen.kt} (86%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{transactions/PurchaseViewModel.kt => home/transactionhistory/TransactionHistoryViewModel.kt} (91%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{transactions => home/transactionhistory}/TransactionInfo.kt (92%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{ => home}/wrapped/WrappedScreen.kt (93%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{ => home}/wrapped/WrappedSlide.kt (89%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{ => home}/wrapped/WrappedViewModel.kt (91%) delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/{HomeSections.kt => NavigationAddServerItem.kt} (52%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/{MeteroidNavSection.kt => NavigationAnimationContainer.kt} (75%) create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt rename app/src/main/kotlin/de/chaosdorf/meteroid/{sync/SyncViewModel.kt => ui/settings/SettingsViewModel.kt} (92%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{users => userlist}/UserListItem.kt (97%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{users => userlist}/UserListScreen.kt (79%) rename app/src/main/kotlin/de/chaosdorf/meteroid/ui/{users => userlist}/UserListViewModel.kt (97%) delete mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/util/BundleExtensions.kt rename app/src/main/kotlin/de/chaosdorf/meteroid/{sample/SampleDrinkProvider.kt => util/HumanReadableHost.kt} (54%) create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackEntryExtensions.kt create mode 100644 app/src/main/kotlin/de/chaosdorf/meteroid/util/NavGraphExtensions.kt create mode 100644 persistence/src/main/kotlin/de/chaosdorf/meteroid/util/GravatarUrl.kt diff --git a/.editorconfig b/.editorconfig index d9f5985..4b10014 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,11 +5,11 @@ insert_final_newline = true indent_style = space indent_size = 4 -[{*.mod, *.dtd, *.ent, *.elt}] +[{*.mod,*.dtd,*.ent,*.elt}] indent_style = space indent_size = 2 -[{*.jhm, *.rng, *.wsdl, *.fxml, *.xslt, *.jrxml, *.ant, *.xul, *.xsl, *.xsd, *.tld, *.jnlp, *.xml}] +[{*.jhm,*.rng,*.wsdl,*.fxml,*.xslt,*.jrxml,*.ant,*.xul,*.xsl,*.xsd,*.tld,*.jnlp,*.xml}] indent_style = space indent_size = 2 @@ -21,10 +21,10 @@ indent_size = 2 indent_style = space indent_size = 2 -[{*.kts, *.kt}] +[{*.kts,*.kt}] indent_style = space indent_size = 2 -[{*.yml, *.yaml}] +[{*.yml,*.yaml}] indent_style = space indent_size = 2 diff --git a/README.md b/README.md index d57404a..996a6b7 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,11 @@  ### What + Small Android application to use with mete, the Matekasse of Chaosdorf. ### Where + * [Chaosdorf Wiki](https://wiki.chaosdorf.de/Meteroid) * [Google Play](https://play.google.com/store/apps/details?id=de.chaosdorf.meteroid2) * [F-Droid](https://f-droid.org/repository/browse/?fdid=de.chaosdorf.meteroid) @@ -20,6 +22,7 @@ Small Android application to use with mete, the Matekasse of Chaosdorf.  ### License + The MIT License (MIT) Copyright (c) 2013-2016 Chaosdorf e.V. @@ -46,17 +49,20 @@ THE SOFTWARE. The meteroid app stores a few things permanently on your device ("permanently" meaning, until you uninstall the app or clear its data): + * the URL of your chosen mete instance * your user ID (just a number that identifies you) * your last five used items Additionally, some data may be cached temporarily: + * your avatar * the avatars of other users * the images of drinks The rest of the data is kept in your mete instance. Please check its terms and make sure you trust its operators: + * your chosen name (this can just be a nickname) * your e-mail address (optional) * your usage history (optional) diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt index 11454ca..49bac13 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/BarcodeId.kt @@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable @JvmInline value class BarcodeId(val value: Long) { override fun toString() = value.toString() + fun isValid() = value >= 0L } diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt index 4670a5f..1677d2c 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/DrinkId.kt @@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable @JvmInline value class DrinkId(val value: Long) { override fun toString() = value.toString() + fun isValid() = value >= 0L } diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt index ae0cee0..054f40c 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/MeteApi.kt @@ -24,9 +24,6 @@ package de.chaosdorf.mete.model -import kotlinx.datetime.Instant -import kotlinx.datetime.LocalDate -import retrofit2.http.Query import java.math.BigDecimal interface MeteApi { diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt new file mode 100644 index 0000000..39880c0 --- /dev/null +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/ServerId.kt @@ -0,0 +1,31 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2023 Chaosdorf e.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package de.chaosdorf.mete.model + +@JvmInline +value class ServerId(val value: Long) { + override fun toString() = value.toString() + fun isValid() = value >= 0L +} diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt index 8b3a85e..e1136cb 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/TransactionId.kt @@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable @JvmInline value class TransactionId(val value: Long) { override fun toString() = value.toString() + fun isValid() = value >= 0L } diff --git a/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt index 0eaacf4..b8f169c 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/model/UserId.kt @@ -30,4 +30,5 @@ import kotlinx.serialization.Serializable @JvmInline value class UserId(val value: Long) { override fun toString() = value.toString() + fun isValid() = value >= 0L } diff --git a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt index cc9f921..f0037dd 100644 --- a/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt +++ b/api/src/main/kotlin/de/chaosdorf/mete/v1/MeteApiV1.kt @@ -24,20 +24,11 @@ package de.chaosdorf.mete.v1 -import de.chaosdorf.mete.model.BarcodeId -import de.chaosdorf.mete.model.DrinkId -import de.chaosdorf.mete.model.MeteApi -import de.chaosdorf.mete.model.PwaManifest -import de.chaosdorf.mete.model.TransactionSummaryModel -import de.chaosdorf.mete.model.UserId -import kotlinx.datetime.Instant -import kotlinx.datetime.LocalDate -import retrofit2.Response +import de.chaosdorf.mete.model.* import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query import java.math.BigDecimal -import java.time.Year internal interface MeteApiV1 : MeteApi { @GET("manifest.json") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8f3c4ae..4aa80fa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> - <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.INTERNET"/> <application android:name=".MeteroidApplication" @@ -9,16 +9,17 @@ android:icon="@mipmap/ic_launcher" android:label="@string/application_name" android:supportsRtl="true" + android:hardwareAccelerated="true" android:usesCleartextTraffic="true"> <activity android:name=".MainActivity" android:exported="true" android:theme="@style/Theme.Meteroid.SplashScreen"> <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <action android:name="android.intent.action.VIEW" /> + <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.LAUNCHER" /> + <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt index 9f09bde..88155e5 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MainActivity.kt @@ -27,13 +27,14 @@ package de.chaosdorf.meteroid import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get import dagger.hilt.android.AndroidEntryPoint -import de.chaosdorf.meteroid.ui.AppRouter -import de.chaosdorf.meteroid.ui.AppViewModel -import de.chaosdorf.meteroid.ui.theme.MeteroidTheme +import de.chaosdorf.meteroid.theme.MeteroidTheme +import de.chaosdorf.meteroid.ui.MeteroidRouter @AndroidEntryPoint @@ -41,15 +42,19 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val viewModelProvider = ViewModelProvider(this) - val viewModel = viewModelProvider.get<AppViewModel>() + val viewModel = viewModelProvider.get<MeteroidViewModel>() installSplashScreen().setKeepOnScreenCondition { - viewModel.initialBackStack.value == null + viewModel.initialAccount.value == null } setContent { + val initialAccount by viewModel.initialAccount.collectAsState() + MeteroidTheme { - AppRouter(viewModel) + if (initialAccount != null) { + MeteroidRouter(initialAccount!!) + } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt similarity index 72% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt index 8e42801..96447ec 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/MeteroidViewModel.kt @@ -22,32 +22,36 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.servers +package de.chaosdorf.meteroid 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.ServerId import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.storage.AccountPreferences -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn +import de.chaosdorf.meteroid.sync.SyncManager +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class ServerListViewModel @Inject constructor( +class MeteroidViewModel @Inject constructor( + accountPreferences: AccountPreferences, serverRepository: ServerRepository, - private val preferences: AccountPreferences + syncManager: SyncManager ) : ViewModel() { - val servers: StateFlow<List<Server>> = serverRepository.getAllFlow() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + val initialAccount: StateFlow<AccountPreferences.State?> = accountPreferences.state + .filterNotNull() + .take(1) + .stateIn(viewModelScope, SharingStarted.Eagerly, null) - fun selectServer(serverId: ServerId) { + init { viewModelScope.launch { - preferences.setServer(serverId) + serverRepository.getAllFlow().collectLatest { list -> + for (server in list) { + syncManager.sync(server, null, incremental = true) + } + } } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt index fc44d7d..71011f4 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/di/DatabaseModule.kt @@ -34,11 +34,7 @@ import dagger.hilt.components.SingletonComponent import de.chaosdorf.mete.model.MeteApiFactory import de.chaosdorf.mete.v1.MeteApiV1Factory import de.chaosdorf.meteroid.MeteroidDatabase -import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.PinnedUserRepository -import de.chaosdorf.meteroid.model.ServerRepository -import de.chaosdorf.meteroid.model.TransactionRepository -import de.chaosdorf.meteroid.model.UserRepository +import de.chaosdorf.meteroid.model.* import javax.inject.Singleton diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt index ebec013..932b7ab 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferences.kt @@ -24,8 +24,8 @@ package de.chaosdorf.meteroid.storage +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.ServerId import kotlinx.coroutines.flow.Flow interface AccountPreferences { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt index e696e02..f74c025 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/storage/AccountPreferencesImpl.kt @@ -29,8 +29,8 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.longPreferencesKey +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.ServerId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.mapLatest import javax.inject.Inject @@ -38,7 +38,6 @@ import javax.inject.Inject class AccountPreferencesImpl @Inject constructor( private val dataStore: DataStore<Preferences> ) : AccountPreferences { - override val state: Flow<AccountPreferences.State> = dataStore.data.mapLatest { val serverId = it[SERVER_KEY] ?: -1L diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt index 000fe2d..0a37a4e 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/AccountProvider.kt @@ -24,13 +24,10 @@ package de.chaosdorf.meteroid.sync +import android.util.Log +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.AccountInfo -import de.chaosdorf.meteroid.model.PinnedUser -import de.chaosdorf.meteroid.model.PinnedUserRepository -import de.chaosdorf.meteroid.model.ServerId -import de.chaosdorf.meteroid.model.ServerRepository -import de.chaosdorf.meteroid.model.UserRepository +import de.chaosdorf.meteroid.model.* import javax.inject.Inject class AccountProvider @Inject constructor( @@ -49,8 +46,10 @@ class AccountProvider @Inject constructor( suspend fun togglePin(serverId: ServerId, userId: UserId) { if (pinnedUserRepository.isPinned(serverId, userId)) { + Log.e("DEBUG", "Unpinning $serverId, $userId") pinnedUserRepository.delete(serverId, userId) } else { + Log.e("DEBUG", "Pinning $serverId, $userId") pinnedUserRepository.save(PinnedUser(serverId, userId)) } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt index b60189c..2af1cce 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/DrinkSyncHandler.kt @@ -27,11 +27,11 @@ package de.chaosdorf.meteroid.sync import androidx.room.withTransaction import de.chaosdorf.mete.model.DrinkId import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.meteroid.MeteroidDatabase import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.sync.base.BaseSyncHandler import javax.inject.Inject @@ -44,7 +44,7 @@ class DrinkSyncHandler @Inject constructor( val server: ServerId, val drink: DrinkId ) - override suspend fun <T>withTransaction(block: suspend () -> T): T = + override suspend fun <T> withTransaction(block: suspend () -> T): T = db.withTransaction(block) override suspend fun store(entry: Drink) = 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 6f8563e..359efb1 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncManager.kt @@ -26,12 +26,10 @@ package de.chaosdorf.meteroid.sync import android.util.Log import de.chaosdorf.mete.model.MeteApiFactory -import de.chaosdorf.meteroid.model.AccountInfo -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerRepository -import de.chaosdorf.meteroid.model.User +import de.chaosdorf.meteroid.model.* +import de.chaosdorf.meteroid.sync.base.SyncHandler import de.chaosdorf.meteroid.util.newServer +import kotlinx.coroutines.flow.combine import java.math.BigDecimal import javax.inject.Inject @@ -42,6 +40,16 @@ class SyncManager @Inject constructor( private val drinkSyncHandler: DrinkSyncHandler, private val transactionSyncHandler: TransactionSyncHandler ) { + val syncState = combine( + userSyncHandler.state, + drinkSyncHandler.state, + transactionSyncHandler.state + ) { states -> + states.find { it is SyncHandler.State.Error } + ?: states.find { it == SyncHandler.State.Loading } + ?: SyncHandler.State.Idle + } + suspend fun checkOffline(server: Server): Boolean { val updated = factory.newServer(server.serverId, server.url) return if (updated == null) { @@ -76,14 +84,16 @@ class SyncManager @Inject constructor( } } - suspend fun purchase(account: AccountInfo, drink: Drink) { + suspend fun purchase(account: AccountInfo, drink: Drink, count: Int) { val api = factory.newInstance(account.server.url) try { Log.i( "SyncManager", "Syncing purchase of ${drink.drinkId}/${drink.drinkId} for ${account.user.name}/${account.user.userId}" ) - api.purchase(account.user.userId, drink.drinkId) + for (i in 0 until count) { + api.purchase(account.user.userId, drink.drinkId) + } sync(account.server, account.user, incremental = true) } catch (e: Exception) { Log.e( diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/TransactionSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/TransactionSyncHandler.kt index e9797e9..88a5841 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/TransactionSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/TransactionSyncHandler.kt @@ -26,20 +26,15 @@ package de.chaosdorf.meteroid.sync import androidx.room.withTransaction import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.TransactionId import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.MeteroidDatabase import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.Transaction import de.chaosdorf.meteroid.model.TransactionRepository import de.chaosdorf.meteroid.sync.base.BaseIncrementalSyncHandler -import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.plus -import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.* import javax.inject.Inject class TransactionSyncHandler @Inject constructor( diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt index 0f50531..13a0847 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/UserSyncHandler.kt @@ -26,10 +26,10 @@ package de.chaosdorf.meteroid.sync import androidx.room.withTransaction import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId 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 de.chaosdorf.meteroid.sync.base.BaseSyncHandler @@ -44,7 +44,7 @@ class UserSyncHandler @Inject constructor( val server: ServerId, val user: UserId ) - override suspend fun <T>withTransaction(block: suspend () -> T): T = + override suspend fun <T> withTransaction(block: suspend () -> T): T = db.withTransaction(block) override suspend fun store(entry: User) = diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseIncrementalSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseIncrementalSyncHandler.kt index 0da6217..27269b1 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseIncrementalSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseIncrementalSyncHandler.kt @@ -37,8 +37,8 @@ abstract class BaseIncrementalSyncHandler<Context, Entry, Key> : } override suspend fun syncIncremental(context: Context) { - if (syncState.compareAndSet(State.Idle, State.Loading) || - syncState.compareAndSet(State.Error(), State.Loading) + if (syncState.compareAndSet(SyncHandler.State.Idle, SyncHandler.State.Loading) || + syncState.compareAndSet(SyncHandler.State.Error(), SyncHandler.State.Loading) ) { Log.w(this::class.simpleName, "Started incremental sync") try { @@ -65,11 +65,11 @@ abstract class BaseIncrementalSyncHandler<Context, Entry, Key> : } } } - syncState.value = State.Idle + syncState.value = SyncHandler.State.Idle Log.w(this::class.simpleName, "Finished incremental sync") } catch (e: Exception) { Log.e(this::class.simpleName, "Error while syncing data", e) - syncState.value = State.Error("Error while syncing data: $e") + syncState.value = SyncHandler.State.Error("Error while syncing data: $e") } } else { Log.w(this::class.simpleName, "Already syncing, disregarding sync request") diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseSyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseSyncHandler.kt index 00d33c1..d9a5aa1 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseSyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/BaseSyncHandler.kt @@ -28,24 +28,8 @@ import android.util.Log import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> { - sealed class State { - data object Idle : State() - data object Loading : 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 <T>withTransaction(block: suspend () -> T): T +abstract class BaseSyncHandler<Context, Entry, Key> : SyncHandler<Context> { + abstract suspend fun <T> withTransaction(block: suspend () -> T): T abstract suspend fun loadCurrent(context: Context): List<Entry> abstract suspend fun loadStored(context: Context): List<Entry> @@ -55,12 +39,13 @@ abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> { abstract suspend fun delete(key: Key) abstract suspend fun store(entry: Entry) - protected val syncState = MutableStateFlow<State>(State.Idle) - val state: StateFlow<State> = syncState + protected val syncState = MutableStateFlow<SyncHandler.State>(SyncHandler.State.Idle) + val state: StateFlow<SyncHandler.State> = syncState override suspend fun sync(context: Context) { - if (syncState.compareAndSet(State.Idle, State.Loading) || - syncState.compareAndSet(State.Error(), State.Loading)) { + if (syncState.compareAndSet(SyncHandler.State.Idle, SyncHandler.State.Loading) || + syncState.compareAndSet(SyncHandler.State.Error(), SyncHandler.State.Loading) + ) { Log.w(this::class.simpleName, "Started sync") try { val loadedEntries = loadCurrent(context) @@ -77,11 +62,11 @@ abstract class BaseSyncHandler<Context, Entry, Key>: SyncHandler<Context> { store(loadedEntry) } } - syncState.value = State.Idle + syncState.value = SyncHandler.State.Idle Log.w(this::class.simpleName, "Finished sync") } catch (e: Exception) { Log.e(this::class.simpleName, "Error while syncing data", e) - syncState.value = State.Error("Error while syncing data: $e") + syncState.value = SyncHandler.State.Error("Error while syncing data: $e") } } else { Log.w(this::class.simpleName, "Already syncing, disregarding sync request") diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/SyncHandler.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/SyncHandler.kt index 7046454..db02ae6 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/SyncHandler.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/sync/base/SyncHandler.kt @@ -26,4 +26,20 @@ package de.chaosdorf.meteroid.sync.base interface SyncHandler<Context> { suspend fun sync(context: Context) + + sealed class State { + data object Idle : State() + data object Loading : 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() + } + } + } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt similarity index 74% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt index e5687bd..75577f7 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Color.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Color.kt @@ -1,4 +1,28 @@ -package de.chaosdorf.meteroid.ui.theme +/* + * 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.theme import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt similarity index 93% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt index 9affa43..83927e0 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Theme.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Theme.kt @@ -22,17 +22,12 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.theme +package de.chaosdorf.meteroid.theme import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.toArgb @@ -110,7 +105,7 @@ private val DarkColors = darkColorScheme( fun MeteroidTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, + dynamicColor: Boolean = false, content: @Composable () -> Unit ) { val colorScheme = when { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt similarity index 92% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt index 3de9104..9e61bf4 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/ThemeGradient.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/ThemeGradient.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.theme +package de.chaosdorf.meteroid.theme import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush @@ -31,9 +31,9 @@ import androidx.compose.ui.graphics.TileMode class ThemeGradient(val colors: List<Color>) { fun linearGradient( - start: Offset = Offset.Zero, - end: Offset = Offset.Infinite, - tileMode: TileMode = TileMode.Clamp + start: Offset = Offset.Zero, + end: Offset = Offset.Infinite, + tileMode: TileMode = TileMode.Clamp ) = Brush.linearGradient(colors, start, end, tileMode) fun verticalGradient( diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Type.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Type.kt similarity index 95% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Type.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/Type.kt index e6759de..afd7a9e 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/theme/Type.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/Type.kt @@ -1,4 +1,4 @@ -package de.chaosdorf.meteroid.ui.theme +package de.chaosdorf.meteroid.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/MeteroidIcons.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/MeteroidIcons.kt similarity index 96% rename from app/src/main/kotlin/de/chaosdorf/meteroid/icons/MeteroidIcons.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/MeteroidIcons.kt index 35ad107..4489383 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/MeteroidIcons.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/MeteroidIcons.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.icons +package de.chaosdorf.meteroid.theme.icons object MeteroidIcons { object Outlined diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/filled/WaterFull.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/filled/WaterFull.kt similarity index 58% rename from app/src/main/kotlin/de/chaosdorf/meteroid/icons/filled/WaterFull.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/filled/WaterFull.kt index 138d742..c4303fc 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/filled/WaterFull.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/filled/WaterFull.kt @@ -1,35 +1,27 @@ /* - * The MIT License (MIT) + * Copyright 2023 The Android Open Source Project * - * Copyright (c) 2013-2023 Chaosdorf e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * 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: + * http://www.apache.org/licenses/LICENSE-2.0 * - * 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. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ -package de.chaosdorf.meteroid.icons.filled +package de.chaosdorf.meteroid.theme.icons.filled import androidx.compose.material.icons.materialPath import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.icons.MeteroidIcons +import de.chaosdorf.meteroid.theme.icons.MeteroidIcons -public val MeteroidIcons.Filled.WaterFull: ImageVector +val MeteroidIcons.Filled.WaterFull: ImageVector get() { if (_waterFull != null) { return _waterFull!! diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/outlined/WaterFull.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/outlined/WaterFull.kt similarity index 95% rename from app/src/main/kotlin/de/chaosdorf/meteroid/icons/outlined/WaterFull.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/outlined/WaterFull.kt index 8f3d075..37d1653 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/outlined/WaterFull.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/outlined/WaterFull.kt @@ -22,14 +22,14 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.icons.outlined +package de.chaosdorf.meteroid.theme.icons.outlined import androidx.compose.material.icons.materialPath import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.icons.MeteroidIcons +import de.chaosdorf.meteroid.theme.icons.MeteroidIcons -public val MeteroidIcons.Outlined.WaterFull: ImageVector +val MeteroidIcons.Outlined.WaterFull: ImageVector get() { if (_waterFull != null) { return _waterFull!! diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/twotone/WaterFull.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/twotone/WaterFull.kt similarity index 96% rename from app/src/main/kotlin/de/chaosdorf/meteroid/icons/twotone/WaterFull.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/twotone/WaterFull.kt index d3eef38..772c5e5 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/icons/twotone/WaterFull.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/theme/icons/twotone/WaterFull.kt @@ -22,14 +22,14 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.icons.twotone +package de.chaosdorf.meteroid.theme.icons.twotone import androidx.compose.material.icons.materialPath import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.icons.MeteroidIcons +import de.chaosdorf.meteroid.theme.icons.MeteroidIcons -public val MeteroidIcons.TwoTone.WaterFull: ImageVector +val MeteroidIcons.TwoTone.WaterFull: ImageVector get() { if (_waterFull != null) { return _waterFull!! diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt deleted file mode 100644 index d10008b..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppRouter.kt +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.ServerId -import de.chaosdorf.meteroid.ui.drinks.DrinkListScreen -import de.chaosdorf.meteroid.ui.drinks.DrinkListViewModel -import de.chaosdorf.meteroid.ui.money.MoneyListScreen -import de.chaosdorf.meteroid.ui.money.MoneyListViewModel -import de.chaosdorf.meteroid.ui.navigation.MeteroidBottomBar -import de.chaosdorf.meteroid.ui.navigation.MeteroidTopBar -import de.chaosdorf.meteroid.ui.navigation.Routes -import de.chaosdorf.meteroid.ui.servers.AddServerScreen -import de.chaosdorf.meteroid.ui.servers.AddServerViewModel -import de.chaosdorf.meteroid.ui.servers.ServerListScreen -import de.chaosdorf.meteroid.ui.servers.ServerListViewModel -import de.chaosdorf.meteroid.ui.transactions.TransactionListScreen -import de.chaosdorf.meteroid.ui.transactions.TransactionViewModel -import de.chaosdorf.meteroid.ui.users.UserListScreen -import de.chaosdorf.meteroid.ui.users.UserListViewModel -import de.chaosdorf.meteroid.ui.wrapped.WrappedScreen -import de.chaosdorf.meteroid.ui.wrapped.WrappedViewModel -import de.chaosdorf.meteroid.util.popUpToRoot - -@Composable -fun AppRouter(viewModel: AppViewModel) { - val navController = rememberNavController() - val snackbarHostState = remember { SnackbarHostState() } - val navigationViewModel = hiltViewModel<NavigationViewModel>() - - val initialBackStack by viewModel.initialBackStack.collectAsState() - val offline by viewModel.offline.collectAsState() - - navController.addOnDestinationChangedListener { _, _, arguments -> - val serverId = arguments?.getLong("server")?.let(::ServerId) - val userId = arguments?.getLong("user")?.let(::UserId) - - navigationViewModel.serverId.value = serverId - navigationViewModel.userId.value = userId - } - - LaunchedEffect(initialBackStack) { - initialBackStack?.let { initialBackStack -> - navController.popUpToRoot() - for (entry in initialBackStack) { - navController.navigate(entry) - } - } - } - - LaunchedEffect(offline) { - snackbarHostState.currentSnackbarData?.dismiss() - if (offline) { - snackbarHostState.showSnackbar( - message = "Unable to connect to server", - duration = SnackbarDuration.Indefinite - ) - } - } - - Box { - Scaffold( - topBar = { - MeteroidTopBar(navController, navigationViewModel, Modifier.align(Alignment.TopCenter)) - }, - bottomBar = { - MeteroidBottomBar(navController, navigationViewModel) - }, - snackbarHost = { - SnackbarHost(hostState = snackbarHostState) - } - ) { paddingValues -> - NavHost( - navController, - startDestination = Routes.Servers.List, - enterTransition = { EnterTransition.None }, - exitTransition = { ExitTransition.None }, - popEnterTransition = { EnterTransition.None }, - popExitTransition = { ExitTransition.None } - ) { - composable(Routes.Servers.Add) { _ -> - val hiltViewModel = hiltViewModel<AddServerViewModel>() - AddServerScreen(navController, hiltViewModel, paddingValues) - } - composable(Routes.Servers.List) { _ -> - val hiltViewModel = hiltViewModel<ServerListViewModel>() - ServerListScreen(navController, hiltViewModel, paddingValues) - } - /* - composable(Routes.Users.Add) { _ -> - AddUserScreen( - hiltViewModel(), - onAdd = { navController.navigate(Routes.Users.List) } - ) - } - */ - composable( - Routes.Users.List, - arguments = listOf( - navArgument("server") { type = NavType.LongType } - ) - ) { _ -> - val hiltViewModel = hiltViewModel<UserListViewModel>() - UserListScreen(navController, hiltViewModel, paddingValues) - } - composable( - Routes.Home.Purchase, - arguments = listOf( - navArgument("server") { type = NavType.LongType }, - navArgument("user") { type = NavType.LongType }, - ) - ) { _ -> - val hiltViewModel = hiltViewModel<DrinkListViewModel>() - DrinkListScreen(navController, hiltViewModel, paddingValues) - } - composable( - Routes.Home.Deposit, - arguments = listOf( - navArgument("server") { type = NavType.LongType }, - navArgument("user") { type = NavType.LongType }, - ) - ) { _ -> - val hiltViewModel = hiltViewModel<MoneyListViewModel>() - MoneyListScreen(navController, hiltViewModel, paddingValues) - } - composable( - Routes.Home.History, - arguments = listOf( - navArgument("server") { type = NavType.LongType }, - navArgument("user") { type = NavType.LongType }, - ) - ) { _ -> - val hiltViewModel = hiltViewModel<TransactionViewModel>() - TransactionListScreen(hiltViewModel, paddingValues) - } - composable( - Routes.Home.Wrapped, - arguments = listOf( - navArgument("server") { type = NavType.LongType }, - navArgument("user") { type = NavType.LongType }, - ) - ) { _ -> - val hiltViewModel = hiltViewModel<WrappedViewModel>() - WrappedScreen(hiltViewModel, paddingValues) - } - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt deleted file mode 100644 index cd55bf2..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/AppViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.meteroid.model.ServerRepository -import de.chaosdorf.meteroid.model.UserRepository -import de.chaosdorf.meteroid.storage.AccountPreferences -import de.chaosdorf.meteroid.sync.SyncManager -import de.chaosdorf.meteroid.ui.navigation.Routes -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -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 - -@HiltViewModel -class AppViewModel @Inject constructor( - accountPreferences: AccountPreferences, - private val serverRepository: ServerRepository, - private val userRepository: UserRepository, - private val syncManager: SyncManager -) : ViewModel() { - val initialBackStack = accountPreferences.state.flatMapLatest { - combine( - serverRepository.countFlow(), - if (it.server == null) flowOf(null) - else serverRepository.getFlow(it.server), - if (it.server == null || it.user == null) flowOf(null) - else userRepository.getFlow(it.server, it.user) - ) { serverCount, server, user -> - if (user != null) { - listOf( - Routes.Servers.List, - Routes.Users.list(user.serverId), - Routes.Home.purchase(user.serverId, user.userId) - ) - } else if (server != null) { - listOf( - Routes.Servers.List, - Routes.Users.list(server.serverId) - ) - } else if (serverCount > 0) { - listOf(Routes.Servers.List) - } else { - listOf(Routes.Servers.Add) - } - } - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - val offline = accountPreferences.state.flatMapLatest { state -> - state.server?.let { serverId -> - serverRepository.getFlow(serverId) - } ?: flowOf(null) - }.mapLatest { server -> - server?.let { syncManager.checkOffline(it) } ?: false - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) - - init { - viewModelScope.launch { - serverRepository.getAllFlow().distinctUntilChanged().collectLatest { list -> - for (server in list) { - syncManager.sync(server, null, incremental = false) - } - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt new file mode 100644 index 0000000..eb711a2 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/Avatar.kt @@ -0,0 +1,114 @@ +/* + * 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.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import de.chaosdorf.meteroid.theme.icons.MeteroidIcons +import de.chaosdorf.meteroid.theme.icons.filled.WaterFull + +@Preview +@Composable +fun UserAvatar( + source: String? = null, +) { + var success by remember { mutableStateOf(false) } + + AvatarLayout( + Modifier.clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer) + ) { + if (!success) { + Icon( + Icons.Filled.Person, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + AsyncImage( + source, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + onSuccess = { success = true }, + onError = { success = false }, + onLoading = { success = false }, + ) + } +} + +@Preview +@Composable +fun ServerAvatar( + source: String? = null, +) { + var success by remember { mutableStateOf(false) } + + AvatarLayout { + if (!success) { + Icon( + MeteroidIcons.Filled.WaterFull, + contentDescription = null + ) + } + AsyncImage( + source, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + onSuccess = { success = true }, + onError = { success = false }, + onLoading = { success = false }, + ) + } +} + +@Composable +fun AvatarLayout( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Box( + modifier.size(36.dp), + contentAlignment = Alignment.Center + ) { + content() + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt new file mode 100644 index 0000000..3f10300 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidRouter.kt @@ -0,0 +1,173 @@ +/* + * 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 android.util.Log +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import de.chaosdorf.mete.model.ServerId +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.meteroid.storage.AccountPreferences +import de.chaosdorf.meteroid.ui.home.deposit.DepositScreen +import de.chaosdorf.meteroid.ui.home.deposit.DepositViewModel +import de.chaosdorf.meteroid.ui.home.purchase.PurchaseScreen +import de.chaosdorf.meteroid.ui.home.purchase.PurchaseViewModel +import de.chaosdorf.meteroid.ui.home.transactionhistory.TransactionHistoryScreen +import de.chaosdorf.meteroid.ui.home.transactionhistory.TransactionHistoryViewModel +import de.chaosdorf.meteroid.ui.home.wrapped.WrappedScreen +import de.chaosdorf.meteroid.ui.home.wrapped.WrappedViewModel +import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel +import de.chaosdorf.meteroid.ui.servers.AddServerScreen +import de.chaosdorf.meteroid.ui.servers.AddServerViewModel +import de.chaosdorf.meteroid.ui.settings.SettingsScreen +import de.chaosdorf.meteroid.ui.settings.SettingsViewModel +import de.chaosdorf.meteroid.ui.userlist.UserListScreen +import de.chaosdorf.meteroid.ui.userlist.UserListViewModel +import de.chaosdorf.meteroid.util.toFancyString +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun MeteroidRouter( + initialAccount: AccountPreferences.State, +) { + val navController = rememberNavController() + val navigationViewModel = hiltViewModel<NavigationViewModel>() + + LaunchedEffect(navController) { + navController.currentBackStack.collectLatest { + Log.i("MeteroidRouter", "Navigation: ${it.toFancyString()}") + } + } + + MeteroidScaffold(navController, navigationViewModel) { paddingValues -> + NavHost( + navController = navController, + startDestination = MeteroidScreen.Home.Purchase.route, + enterTransition = { fadeIn() }, + exitTransition = { fadeOut() }, + popEnterTransition = { fadeIn() }, + popExitTransition = { fadeOut() }, + modifier = Modifier.padding(paddingValues) + ) { + composable( + MeteroidScreen.Home.Purchase.route, arguments = listOf( + navArgument("server") { + type = NavType.LongType + initialAccount.server?.let { defaultValue = it.value } + }, + navArgument("user") { + type = NavType.LongType + initialAccount.user?.let { defaultValue = it.value } + }, + ) + ) { entry -> + val serverId = entry.arguments?.getLong("server")?.let(::ServerId) + val userId = entry.arguments?.getLong("user")?.let(::UserId) + + LaunchedEffect(serverId, userId) { + if (serverId == null || userId == null) { + navigationViewModel.expanded.value = true + } + } + + if (serverId != null && userId != null) { + val viewModel: PurchaseViewModel = hiltViewModel( + key = MeteroidScreen.Home.Purchase.build(serverId, userId) + ) + PurchaseScreen(navController, viewModel, PaddingValues(top = 96.dp)) + } + } + composable( + MeteroidScreen.Home.Deposit.route, arguments = listOf( + navArgument("server") { + type = NavType.LongType + }, + navArgument("user") { + type = NavType.LongType + }, + ) + ) { + val viewModel: DepositViewModel = hiltViewModel() + DepositScreen(navController, viewModel, PaddingValues(top = 96.dp)) + } + composable( + MeteroidScreen.Home.History.route, arguments = listOf( + navArgument("server") { + type = NavType.LongType + }, + navArgument("user") { + type = NavType.LongType + }, + ) + ) { + val viewModel: TransactionHistoryViewModel = hiltViewModel() + TransactionHistoryScreen(viewModel, PaddingValues(top = 96.dp)) + } + composable( + MeteroidScreen.Home.Wrapped.route, arguments = listOf( + navArgument("server") { + type = NavType.LongType + }, + navArgument("user") { + type = NavType.LongType + }, + ) + ) { + val viewModel: WrappedViewModel = hiltViewModel() + WrappedScreen(viewModel, PaddingValues(top = 96.dp)) + } + composable( + MeteroidScreen.UserList.route, arguments = listOf( + navArgument("server") { + type = NavType.LongType + }, + ) + ) { + val viewModel: UserListViewModel = hiltViewModel() + UserListScreen(navController, viewModel, PaddingValues(top = 96.dp)) + } + composable(MeteroidScreen.AddServer.route) { + val viewModel: AddServerViewModel = hiltViewModel() + AddServerScreen(navController, viewModel, PaddingValues(top = 96.dp)) + } + composable(MeteroidScreen.Settings.route) { + val viewModel: SettingsViewModel = hiltViewModel() + SettingsScreen(navController, viewModel, PaddingValues(top = 96.dp)) + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt new file mode 100644 index 0000000..2867b47 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScaffold.kt @@ -0,0 +1,51 @@ +/* + * 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.PaddingValues +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import de.chaosdorf.meteroid.ui.home.MeteroidBottomBar +import de.chaosdorf.meteroid.ui.navigation.MeteroidNavigation +import de.chaosdorf.meteroid.ui.navigation.NavigationScrim +import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel + +@Composable +fun MeteroidScaffold( + navController: NavController, + viewModel: NavigationViewModel, + content: @Composable (PaddingValues) -> Unit +) { + Box { + Scaffold( + bottomBar = { MeteroidBottomBar(navController, viewModel) }, + content = content + ) + NavigationScrim(viewModel) + MeteroidNavigation(navController, viewModel) + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt new file mode 100644 index 0000000..f0c2724 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/MeteroidScreen.kt @@ -0,0 +1,118 @@ +/* + * 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.material.icons.Icons +import androidx.compose.material.icons.outlined.Celebration +import androidx.compose.material.icons.outlined.History +import androidx.compose.material.icons.outlined.LocalAtm +import androidx.compose.material.icons.twotone.Celebration +import androidx.compose.material.icons.twotone.History +import androidx.compose.material.icons.twotone.LocalAtm +import androidx.compose.ui.graphics.vector.ImageVector +import de.chaosdorf.mete.model.ServerId +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.meteroid.theme.icons.MeteroidIcons +import de.chaosdorf.meteroid.theme.icons.outlined.WaterFull +import de.chaosdorf.meteroid.theme.icons.twotone.WaterFull + +sealed class MeteroidScreen(val route: String) { + sealed class Home( + val label: String, + val activeIcon: ImageVector, + val inactiveIcon: ImageVector, + route: String + ) : MeteroidScreen(route) { + data object Purchase : Home( + "Purchase", + MeteroidIcons.TwoTone.WaterFull, + MeteroidIcons.Outlined.WaterFull, + "server/{server}/user/{user}/purchase" + ) { + fun build(server: ServerId, user: UserId) = route + .replace("{server}", server.value.toString()) + .replace("{user}", user.value.toString()) + } + + data object Deposit : Home( + "Deposit", + Icons.TwoTone.LocalAtm, + Icons.Outlined.LocalAtm, + "server/{server}/user/{user}/deposit" + ) { + fun build(server: ServerId, user: UserId) = route + .replace("{server}", server.value.toString()) + .replace("{user}", user.value.toString()) + } + + data object History : Home( + "History", + Icons.TwoTone.History, + Icons.Outlined.History, + "server/{server}/user/{user}/history" + ) { + fun build(server: ServerId, user: UserId) = route + .replace("{server}", server.value.toString()) + .replace("{user}", user.value.toString()) + } + + data object Wrapped : Home( + "Wrapped", + Icons.TwoTone.Celebration, + Icons.Outlined.Celebration, + "server/{server}/user/{user}/wrapped" + ) { + fun build(server: ServerId, user: UserId) = route + .replace("{server}", server.value.toString()) + .replace("{user}", user.value.toString()) + } + } + + data object UserList : MeteroidScreen("server/{server}/userList") { + fun build(server: ServerId) = route + .replace("{server}", server.value.toString()) + } + + data object AddServer : MeteroidScreen("addServer") { + fun build() = route + } + + data object Settings : MeteroidScreen("settings") { + fun build() = route + } + + companion object { + fun byRoute(route: String?) = when (route) { + Home.Purchase.route -> Home.Purchase + Home.Deposit.route -> Home.Deposit + Home.History.route -> Home.History + Home.Wrapped.route -> Home.Wrapped + UserList.route -> UserList + AddServer.route -> AddServer + Settings.route -> Settings + else -> null + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt deleted file mode 100644 index 29cd7a4..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/NavigationViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.model.UserId -import de.chaosdorf.meteroid.model.PinnedUserRepository -import de.chaosdorf.meteroid.model.ServerId -import de.chaosdorf.meteroid.model.ServerRepository -import de.chaosdorf.meteroid.model.UserRepository -import de.chaosdorf.meteroid.sync.AccountProvider -import de.chaosdorf.meteroid.sync.SyncManager -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class NavigationViewModel @Inject constructor( - syncManager: SyncManager, - serverRepository: ServerRepository, - userRepository: UserRepository, - pinnedUserRepository: PinnedUserRepository, - private val accountProvider: AccountProvider -) : ViewModel() { - val serverId = MutableStateFlow<ServerId?>(null) - val userId = MutableStateFlow<UserId?>(null) - - val server = serverId.flatMapLatest { - if (it == null) flowOf(null) - else serverRepository.getFlow(it) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - val user = combine(serverId, userId) { serverId, userId -> - if (serverId == null || userId == null) null - else Pair(serverId, userId) - }.flatMapLatest { - if (it == null) flowOf(null) - else userRepository.getFlow(it.first, it.second) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - val pinned = combine(serverId, userId) { serverId, userId -> - if (serverId == null || userId == null) null - else Pair(serverId, userId) - }.flatMapLatest { - if (it == null) flowOf(null) - else pinnedUserRepository.isPinnedFlow(it.first, it.second) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) - - init { - viewModelScope.launch { - combine(server, user) { server, user -> - server?.let { Pair(server, user) } - }.distinctUntilChanged().collectLatest { account -> - account?.let { (server, user) -> - syncManager.sync(server, user, incremental = true) - } - } - } - } - - fun togglePin(serverId: ServerId, userId: UserId) { - viewModelScope.launch { - accountProvider.togglePin(serverId, userId) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt new file mode 100644 index 0000000..789047b --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBar.kt @@ -0,0 +1,130 @@ +/* + * 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.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState +import de.chaosdorf.meteroid.ui.MeteroidScreen +import de.chaosdorf.meteroid.ui.navigation.NavigationViewModel +import de.chaosdorf.meteroid.util.findStartDestination +import kotlinx.datetime.Clock +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +@Composable +fun MeteroidBottomBar(navController: NavController, viewModel: NavigationViewModel) { + val backStackEntry by navController.currentBackStackEntryAsState() + val activeRoute = MeteroidScreen.byRoute(backStackEntry?.destination?.route) + + val account by viewModel.account.collectAsState() + val server = account?.server + val user = account?.user + val historyDisabled by viewModel.historyDisabled.collectAsState() + val wrappedEnabled = Clock.System.now() + .toLocalDateTime(TimeZone.currentSystemDefault()) + .month.let { it == java.time.Month.DECEMBER } + + AnimatedVisibility( + activeRoute is MeteroidScreen.Home && server != null && user != null, + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }) + ) { + NavigationBar( + contentColor = MaterialTheme.colorScheme.primary + ) { + MeteroidBottomBarItem( + MeteroidScreen.Home.Purchase, + activeRoute == MeteroidScreen.Home.Purchase, + ) { + if (server != null && user != null) { + navController.navigate(MeteroidScreen.Home.Purchase.build(server, user)) { + launchSingleTop = true + restoreState = false + popUpTo(findStartDestination(navController.graph).id) { + saveState = false + } + } + } + } + + MeteroidBottomBarItem( + MeteroidScreen.Home.Deposit, + activeRoute == MeteroidScreen.Home.Deposit, + ) { + if (server != null && user != null) { + navController.navigate(MeteroidScreen.Home.Deposit.build(server, user)) { + launchSingleTop = true + restoreState = false + popUpTo(findStartDestination(navController.graph).id) { + saveState = false + } + } + } + } + + if (!historyDisabled) { + MeteroidBottomBarItem( + MeteroidScreen.Home.History, + activeRoute == MeteroidScreen.Home.History, + ) { + if (server != null && user != null) { + navController.navigate(MeteroidScreen.Home.History.build(server, user)) { + launchSingleTop = true + restoreState = false + popUpTo(findStartDestination(navController.graph).id) { + saveState = false + } + } + } + } + + if (wrappedEnabled) { + MeteroidBottomBarItem( + MeteroidScreen.Home.Wrapped, + activeRoute == MeteroidScreen.Home.Wrapped, + ) { + if (server != null && user != null) { + navController.navigate(MeteroidScreen.Home.Wrapped.build(server, user)) { + launchSingleTop = true + restoreState = false + popUpTo(findStartDestination(navController.graph).id) { + saveState = false + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt new file mode 100644 index 0000000..052ee59 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/MeteroidBottomBarItem.kt @@ -0,0 +1,54 @@ +/* + * 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.foundation.layout.RowScope +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import de.chaosdorf.meteroid.theme.onPrimaryContainerTinted +import de.chaosdorf.meteroid.ui.MeteroidScreen + +@Composable +fun RowScope.MeteroidBottomBarItem( + route: MeteroidScreen.Home, + active: Boolean, + onClick: () -> Unit, +) { + NavigationBarItem( + icon = { + Icon( + if (active) route.activeIcon else route.inactiveIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainerTinted + ) + }, + label = { Text(route.label, color = MaterialTheme.colorScheme.onPrimaryContainerTinted) }, + selected = active, + onClick = onClick, + colors = NavigationBarItemDefaults.colors( + indicatorColor = MaterialTheme.colorScheme.primaryContainer + ) + ) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt similarity index 97% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt index 2231c37..8a90ba3 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/PriceBadge.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/PriceBadge.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui +package de.chaosdorf.meteroid.ui.home import androidx.compose.foundation.layout.padding import androidx.compose.material3.Badge @@ -33,7 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import java.math.BigDecimal @Composable diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt similarity index 85% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt index c503e5e..1e83c4f 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyTile.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositMoneyItem.kt @@ -22,15 +22,11 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.money +package de.chaosdorf.meteroid.ui.home.deposit import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -39,10 +35,10 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import de.chaosdorf.meteroid.ui.PriceBadge +import de.chaosdorf.meteroid.ui.home.PriceBadge @Composable -fun MoneyTile( +fun DepositMoneyItem( item: MonetaryAmount, modifier: Modifier = Modifier, onDeposit: (MonetaryAmount) -> Unit = {} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt similarity index 91% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt index ee78739..339520e 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositScreen.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.money +package de.chaosdorf.meteroid.ui.home.deposit import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding @@ -35,9 +35,9 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController @Composable -fun MoneyListScreen( +fun DepositScreen( navController: NavController, - viewModel: MoneyListViewModel, + viewModel: DepositViewModel, contentPadding: PaddingValues = PaddingValues(), ) { LazyVerticalGrid( @@ -47,9 +47,9 @@ fun MoneyListScreen( ) { items( viewModel.money, - key = { "${it.ordinal}" }, + key = { "deposit-${it.ordinal}" }, ) { monetaryAmount -> - MoneyTile(monetaryAmount) { + DepositMoneyItem(monetaryAmount) { viewModel.deposit(it, navController::navigateUp) } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt similarity index 75% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt index 105d5db..82ad5ee 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/money/MoneyListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/DepositViewModel.kt @@ -22,34 +22,21 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.money +package de.chaosdorf.meteroid.ui.home.deposit -import androidx.annotation.DrawableRes 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.R -import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.sync.AccountProvider import de.chaosdorf.meteroid.sync.SyncManager import kotlinx.coroutines.launch -import java.math.BigDecimal import javax.inject.Inject -enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) { - MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50), - MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100), - MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200), - MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500), - MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000), - MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000), - MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000), -} - @HiltViewModel -class MoneyListViewModel @Inject constructor( +class DepositViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val accountProvider: AccountProvider, private val syncManager: SyncManager, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt new file mode 100644 index 0000000..a38f630 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/deposit/MonetaryAmount.kt @@ -0,0 +1,39 @@ +/* + * 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.deposit + +import androidx.annotation.DrawableRes +import de.chaosdorf.meteroid.R +import java.math.BigDecimal + +enum class MonetaryAmount(val amount: BigDecimal, @DrawableRes val image: Int) { + MONEY_50(0.50.toBigDecimal(), R.drawable.euro_50), + MONEY_100(1.00.toBigDecimal(), R.drawable.euro_100), + MONEY_200(2.00.toBigDecimal(), R.drawable.euro_200), + MONEY_500(5.00.toBigDecimal(), R.drawable.euro_500), + MONEY_1000(10.00.toBigDecimal(), R.drawable.euro_1000), + MONEY_2000(20.00.toBigDecimal(), R.drawable.euro_2000), + MONEY_5000(50.00.toBigDecimal(), R.drawable.euro_5000), +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt similarity index 74% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt index 9a6d972..274c9b1 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkTile.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseDrinkTile.kt @@ -22,26 +22,17 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.drinks +package de.chaosdorf.meteroid.ui.home.purchase import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -49,23 +40,30 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil.compose.rememberAsyncImagePainter import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.sample.SampleDrinkProvider -import de.chaosdorf.meteroid.ui.PriceBadge -import de.chaosdorf.meteroid.ui.theme.secondaryGradient +import de.chaosdorf.meteroid.theme.secondaryGradient +import de.chaosdorf.meteroid.ui.home.PriceBadge +import kotlinx.coroutines.delay import java.math.BigDecimal -@Preview(widthDp = 120, showBackground = true) @Composable -fun DrinkTile( - @PreviewParameter(SampleDrinkProvider::class) item: Drink, +fun PurchaseDrinkTile( + item: Drink, modifier: Modifier = Modifier, - onPurchase: (Drink) -> Unit = {} + onPurchase: (Drink, Int) -> Unit = { _, _ -> } ) { + var purchaseCount by remember { mutableStateOf(0) } + val pendingPurchases = purchaseCount != 0 + + LaunchedEffect(purchaseCount) { + delay(2000L) + onPurchase(item, purchaseCount) + purchaseCount = 0 + } + val thumbPainter = rememberAsyncImagePainter( item.logoUrl ) @@ -79,25 +77,36 @@ fun DrinkTile( .height(IntrinsicSize.Max) .alpha(if (item.active) 1.0f else 0.67f) .clip(RoundedCornerShape(8.dp)) - .clickable { onPurchase(item) } + .clickable { purchaseCount += 1 } .padding(8.dp) ) { - Box { + Box( + Modifier.aspectRatio(1.0f) + .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient(), CircleShape), + contentAlignment = Alignment.Center + ) { Image( drinkPainter, contentDescription = null, contentScale = ContentScale.Fit, - modifier = Modifier - .aspectRatio(1.0f) + modifier = Modifier.alpha(if (pendingPurchases) 0.0f else 1.0f) .clip(CircleShape) - .background(MaterialTheme.colorScheme.secondaryGradient.verticalGradient()) ) PriceBadge( item.price, modifier = Modifier + .alpha(if (pendingPurchases) 0.0f else 1.0f) .align(Alignment.BottomEnd) .paddingFromBaseline(bottom = 12.dp) ) + Text( + "×$purchaseCount", + fontSize = 36.sp, + fontWeight = FontWeight.Light, + color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.67f), + textAlign = TextAlign.Center, + modifier = Modifier.alpha(if (pendingPurchases) 1.0f else 0.0f) + ) } Spacer(Modifier.height(4.dp)) Text( diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt similarity index 87% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt index cba1aa8..807b2ec 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListFilterChip.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseFilterChip.kt @@ -22,22 +22,18 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.drinks +package de.chaosdorf.meteroid.ui.home.purchase import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun DrinkListFilterChip( +fun PurchaseFilterChip( label: String, selected: Boolean, onClick: () -> Unit, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt similarity index 72% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt index 3e7db6e..74f49eb 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseScreen.kt @@ -22,13 +22,9 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.drinks +package de.chaosdorf.meteroid.ui.home.purchase -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -42,9 +38,9 @@ import androidx.navigation.NavController @OptIn(ExperimentalLayoutApi::class) @Composable -fun DrinkListScreen( +fun PurchaseScreen( navController: NavController, - viewModel: DrinkListViewModel, + viewModel: PurchaseViewModel, contentPadding: PaddingValues = PaddingValues(), ) { val drinks by viewModel.drinks.collectAsState() @@ -60,25 +56,25 @@ fun DrinkListScreen( modifier = Modifier.padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - DrinkListFilterChip( + PurchaseFilterChip( label = "Active", - selected = filters.contains(DrinkListViewModel.Filter.Active), - onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.Active) } + selected = filters.contains(PurchaseViewModel.Filter.Active), + onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.Active) } ) - DrinkListFilterChip( + PurchaseFilterChip( label = "Coffeine Free", - selected = filters.contains(DrinkListViewModel.Filter.CaffeineFree), - onClick = { viewModel.toggleFilter(DrinkListViewModel.Filter.CaffeineFree) } + selected = filters.contains(PurchaseViewModel.Filter.CaffeineFree), + onClick = { viewModel.toggleFilter(PurchaseViewModel.Filter.CaffeineFree) } ) } } items( drinks, - key = { "${it.serverId}-${it.drinkId}" }, + key = { "drink-${it.serverId}-${it.drinkId}" }, ) { drink -> - DrinkTile(drink) { - viewModel.purchase(it, navController::navigateUp) + PurchaseDrinkTile(drink) { item, count -> + viewModel.purchase(item, count, navController::navigateUp) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt similarity index 89% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt index 59f2d53..1630204 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/drinks/DrinkListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/purchase/PurchaseViewModel.kt @@ -22,16 +22,16 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.drinks +package de.chaosdorf.meteroid.ui.home.purchase 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.Drink import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.sync.AccountProvider import de.chaosdorf.meteroid.sync.SyncManager import de.chaosdorf.meteroid.util.update @@ -43,14 +43,14 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class DrinkListViewModel @Inject constructor( +class PurchaseViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val accountProvider: AccountProvider, private val syncManager: SyncManager, drinkRepository: DrinkRepository, ) : ViewModel() { - private val serverId = ServerId(checkNotNull(savedStateHandle["server"])) - private val userId = UserId(checkNotNull(savedStateHandle["user"])) + val serverId = ServerId(checkNotNull(savedStateHandle["server"])) + val userId = UserId(checkNotNull(savedStateHandle["user"])) val filters: StateFlow<Set<Filter>> = savedStateHandle.getStateFlow("filters", setOf(Filter.Active)) @@ -71,10 +71,10 @@ class DrinkListViewModel @Inject constructor( } } - fun purchase(item: Drink, onBack: () -> Unit) { + fun purchase(item: Drink, count: Int, onBack: () -> Unit) { viewModelScope.launch { accountProvider.account(serverId, userId)?.let { account -> - syncManager.purchase(account, item) + syncManager.purchase(account, item, count) if (!account.pinned) { onBack() } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt similarity index 90% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt index 7b5eaad..71d5353 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListItem.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryItem.kt @@ -22,15 +22,11 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.transactions +package de.chaosdorf.meteroid.ui.home.transactionhistory import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AttachMoney @@ -48,8 +44,8 @@ import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.Transaction -import de.chaosdorf.meteroid.ui.PriceBadge -import de.chaosdorf.meteroid.ui.theme.secondaryGradient +import de.chaosdorf.meteroid.theme.secondaryGradient +import de.chaosdorf.meteroid.ui.home.PriceBadge import kotlinx.datetime.TimeZone import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toLocalDateTime @@ -58,7 +54,7 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @Composable -fun TransactionListItem( +fun TransactionHistoryItem( transaction: Transaction, drink: Drink?, modifier: Modifier = Modifier diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt similarity index 86% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt index 7d208d9..f6cbec9 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryScreen.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.transactions +package de.chaosdorf.meteroid.ui.home.transactionhistory import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.lazy.LazyColumn @@ -32,8 +32,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @Composable -fun TransactionListScreen( - viewModel: TransactionViewModel, +fun TransactionHistoryScreen( + viewModel: TransactionHistoryViewModel, contentPadding: PaddingValues = PaddingValues(), ) { val transactions by viewModel.transactions.collectAsState() @@ -41,9 +41,9 @@ fun TransactionListScreen( LazyColumn(contentPadding = contentPadding) { items( transactions, - key = { "${it.transaction.serverId}-${it.transaction.transactionId}" }, + key = { "transaction-${it.transaction.serverId}-${it.transaction.transactionId}" }, ) { (transaction, drink) -> - TransactionListItem(transaction, drink) + TransactionHistoryItem(transaction, drink) } } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt similarity index 91% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt index 4936086..0ad8aac 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/PurchaseViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionHistoryViewModel.kt @@ -22,30 +22,26 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.transactions +package de.chaosdorf.meteroid.ui.home.transactionhistory 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.DrinkRepository -import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.TransactionRepository import de.chaosdorf.meteroid.sync.AccountProvider import de.chaosdorf.meteroid.sync.SyncManager -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import java.math.BigDecimal import javax.inject.Inject import kotlin.time.Duration.Companion.minutes @HiltViewModel -class TransactionViewModel @Inject constructor( +class TransactionHistoryViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val accountProvider: AccountProvider, private val syncManager: SyncManager, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt similarity index 92% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt index c4654ce..15e2038 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/transactions/TransactionInfo.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/transactionhistory/TransactionInfo.kt @@ -22,12 +22,12 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.transactions +package de.chaosdorf.meteroid.ui.home.transactionhistory import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.Transaction data class TransactionInfo( - val transaction: Transaction, - val drink: Drink? + val transaction: Transaction, + val drink: Drink? ) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt similarity index 93% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt index 6d8e2cc..c08ee4d 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedScreen.kt @@ -22,8 +22,10 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.wrapped +package de.chaosdorf.meteroid.ui.home.wrapped +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import androidx.compose.foundation.Image import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.size @@ -49,11 +51,16 @@ fun WrappedScreen( contentPadding: PaddingValues = PaddingValues(), ) { val slides by viewModel.slides.collectAsState() + @Suppress("DEPRECATION") + val locale = LocalConfiguration.current.let { + if (VERSION.SDK_INT >= VERSION_CODES.N) it.locales.get(0) + else it.locale + } LazyColumn(contentPadding = contentPadding) { items( slides, - key = { "${it::class.qualifiedName}" }, + key = { "wrapped-${it::class.simpleName}" }, ) { slide -> when (slide) { is WrappedSlide.MostBoughtDrink -> @@ -124,10 +131,7 @@ fun WrappedScreen( headlineContent = { Text( "You were most active on ${ - slide.weekday.getDisplayName( - TextStyle.FULL, - LocalConfiguration.current.locale - ) + slide.weekday.getDisplayName(TextStyle.FULL, locale) }s at ${slide.hour} o'clock." ) }, diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt similarity index 89% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt index c326c1a..ec264b6 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedSlide.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedSlide.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.wrapped +package de.chaosdorf.meteroid.ui.home.wrapped import de.chaosdorf.mete.model.DrinkId import de.chaosdorf.meteroid.model.Drink @@ -34,19 +34,19 @@ import kotlinx.datetime.toLocalDateTime sealed class WrappedSlide { interface Factory { fun create( - transactions: List<Transaction>, - drinks: Map<DrinkId, Drink> + transactions: List<Transaction>, + drinks: Map<DrinkId, Drink> ): WrappedSlide? } data class MostBoughtDrink( - val drink: Drink, - val count: Int, + val drink: Drink, + val count: Int, ) : WrappedSlide() { companion object : Factory { override fun create( - transactions: List<Transaction>, - drinks: Map<DrinkId, Drink> + transactions: List<Transaction>, + drinks: Map<DrinkId, Drink> ): WrappedSlide? = transactions .mapNotNull { drinks[it.drinkId] } .groupingBy { it } @@ -84,8 +84,8 @@ sealed class WrappedSlide { companion object : Factory { override fun create( - transactions: List<Transaction>, - drinks: Map<DrinkId, Drink> + transactions: List<Transaction>, + drinks: Map<DrinkId, Drink> ): WrappedSlide = transactions .mapNotNull { drinks[it.drinkId] } .mapNotNull { drink -> drink.caffeine?.let { it * drink.volume.toDouble() * 10 } } @@ -93,7 +93,7 @@ sealed class WrappedSlide { .let { dosage -> Caffeine( dosage, - Animal.values() + Animal.entries .sortedBy(Animal::lethalDosage) .lastOrNull { it.lethalDosage < dosage } ) @@ -102,13 +102,13 @@ sealed class WrappedSlide { } data class MostActive( - val weekday: DayOfWeek, - val hour: Int, + val weekday: DayOfWeek, + val hour: Int, ) : WrappedSlide() { companion object : Factory { override fun create( - transactions: List<Transaction>, - drinks: Map<DrinkId, Drink> + transactions: List<Transaction>, + drinks: Map<DrinkId, Drink> ): WrappedSlide? = transactions .map { it.timestamp.toLocalDateTime(TimeZone.currentSystemDefault()) } .groupingBy { Pair(it.dayOfWeek, it.hour) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt similarity index 91% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt index a3fceec..91e1f7d 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/wrapped/WrappedViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/home/wrapped/WrappedViewModel.kt @@ -22,29 +22,24 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.wrapped +package de.chaosdorf.meteroid.ui.home.wrapped import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.chaosdorf.mete.model.DrinkId +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId import de.chaosdorf.meteroid.model.Drink import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.Transaction import de.chaosdorf.meteroid.model.TransactionRepository import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import kotlinx.datetime.Clock -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.Month -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toInstant -import kotlinx.datetime.toLocalDateTime +import kotlinx.datetime.* import javax.inject.Inject @HiltViewModel diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt deleted file mode 100644 index a8c0369..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidBottomBar.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.navigation - -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.navigation.NavController -import androidx.navigation.compose.currentBackStackEntryAsState -import de.chaosdorf.meteroid.ui.NavigationViewModel -import de.chaosdorf.meteroid.ui.theme.onPrimaryContainerTinted -import de.chaosdorf.meteroid.util.popUpToRoot -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import java.time.Month - -@Composable -fun MeteroidBottomBar( - navController: NavController, - viewModel: NavigationViewModel -) { - val user by viewModel.user.collectAsState() - val historyEnabled = user?.audit == true - val wrappedEnabled = Clock.System.now() - .toLocalDateTime(TimeZone.currentSystemDefault()) - .month.let { it == Month.NOVEMBER || it == Month.DECEMBER } - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentDestination = navBackStackEntry?.destination - val activeRoute = if (user == null) null - else HomeSections.entries.find { - it.route == currentDestination?.route - } - - if (activeRoute != null) { - NavigationBar( - contentColor = MaterialTheme.colorScheme.onPrimaryContainerTinted - ) { - for (route in HomeSections.entries) { - if (wrappedEnabled || route != HomeSections.WRAPPED) { - NavigationBarItem( - icon = { - Icon( - if (route == activeRoute) route.iconActive else route.icon, - contentDescription = route.title, - tint = MaterialTheme.colorScheme.onPrimaryContainerTinted - ) - }, - label = { Text(route.title, color = MaterialTheme.colorScheme.onPrimaryContainerTinted) }, - selected = route == activeRoute, - onClick = { - navController.popUpToRoot() - navController.navigate(Routes.Servers.List) - navController.navigate(Routes.Users.list(user!!.serverId)) - navController.navigate( - route.withArguments( - "server" to user!!.serverId.value.toString(), - "user" to user!!.userId.value.toString() - ) - ) - }, - enabled = when (route) { - HomeSections.PURCHASE, - HomeSections.DEPOSIT -> true - - HomeSections.HISTORY, - HomeSections.WRAPPED -> historyEnabled - } - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt new file mode 100644 index 0000000..3b5cab6 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavigation.kt @@ -0,0 +1,171 @@ +/* + * 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.navigation + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState +import de.chaosdorf.mete.model.ServerId +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.meteroid.storage.AccountPreferences +import de.chaosdorf.meteroid.sync.base.SyncHandler +import de.chaosdorf.meteroid.ui.MeteroidScreen +import de.chaosdorf.meteroid.util.findStartDestination + +@Composable +fun MeteroidNavigation(navController: NavController, viewModel: NavigationViewModel) { + val backStackEntry by navController.currentBackStackEntryAsState() + val activeRoute = MeteroidScreen.byRoute(backStackEntry?.destination?.route) + + val expanded by viewModel.expanded.collectAsState() + val account by viewModel.account.collectAsState() + val entries by viewModel.entries.collectAsState() + val syncState by viewModel.syncState.collectAsState() + + LaunchedEffect(navController) { + navController.addOnDestinationChangedListener { _, _, arguments -> + val serverId = arguments?.getLong("server")?.let(::ServerId) + val userId = arguments?.getLong("user")?.let(::UserId) + + viewModel.account.value = AccountPreferences.State(serverId, userId) + viewModel.expanded.value = false + } + } + + BackHandler(expanded) { + viewModel.expanded.value = false + } + + val verticalContentPadding: Dp by animateDpAsState(if (expanded) 4.dp else 0.dp, label = "verticalContentPadding") + val shadowElevation: Dp by animateDpAsState(if (expanded) 16.dp else 4.dp, label = "shadowElevation") + + Surface( + Modifier.fillMaxWidth().animateContentSize() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 128.dp), + shape = RoundedCornerShape(32.dp), + shadowElevation = shadowElevation, + tonalElevation = 8.dp + ) { + Box { + LazyColumn(contentPadding = PaddingValues(vertical = verticalContentPadding)) { + items(entries, key = NavigationElement::key) { entry -> + val isCurrent = entry.isCurrent(activeRoute, account?.server, account?.user) + + NavigationAnimationContainer( + expanded || isCurrent + ) { + when (entry) { + is NavigationElement.ServerElement -> NavigationServerItem(expanded, entry.server) { + viewModel.expanded.value = true + } + + is NavigationElement.UserElement -> NavigationUserItem(expanded, + entry.user, + entry.pinned, + viewModel::togglePin, + onExpand = { viewModel.expanded.value = true } + ) { + if (isCurrent) { + viewModel.expanded.value = false + } else { + navController.navigate( + MeteroidScreen.Home.Purchase.build( + entry.user.serverId, entry.user.userId + ) + ) { + launchSingleTop = true + restoreState = false + popUpTo(findStartDestination(navController.graph).id) { + saveState = false + } + } + } + } + + is NavigationElement.UserListElement -> NavigationUserListItem { + if (NavigationElement.ServerElement(entry.server) + .isCurrent(activeRoute, account?.server, account?.user) + ) { + viewModel.expanded.value = false + } else { + navController.navigate(MeteroidScreen.UserList.build(entry.server.serverId)) { + launchSingleTop = true + restoreState = false + popUpTo(findStartDestination(navController.graph).id) { + saveState = false + } + } + } + } + + NavigationElement.AddServerElement -> NavigationAddServerItem(expanded, + onExpand = { viewModel.expanded.value = true } + ) { + if (isCurrent) { + viewModel.expanded.value = false + } else { + navController.navigate(MeteroidScreen.AddServer.build()) + } + } + + NavigationElement.SettingsElement -> NavigationSettingsItem( + expanded, + onExpand = { viewModel.expanded.value = true } + ) { + if (isCurrent) { + viewModel.expanded.value = false + } else { + navController.navigate(MeteroidScreen.Settings.build()) + } + } + } + } + } + } + AnimatedVisibility(syncState == SyncHandler.State.Loading, enter = fadeIn(), exit = fadeOut()) { + LinearProgressIndicator(Modifier.align(Alignment.TopCenter).requiredHeight(2.dp).fillMaxWidth()) + } + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt deleted file mode 100644 index a653449..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidTopBar.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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.navigation - -import android.util.Log -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.PushPin -import androidx.compose.material.icons.outlined.PushPin -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -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.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import de.chaosdorf.meteroid.R -import de.chaosdorf.meteroid.ui.NavigationViewModel -import de.chaosdorf.meteroid.ui.PriceBadge -import de.chaosdorf.meteroid.util.rememberAvatarPainter -import okhttp3.HttpUrl.Companion.toHttpUrl - -@Composable -fun MeteroidTopBar( - navController: NavController, - viewModel: NavigationViewModel, - modifier: Modifier = Modifier -) { - val server by viewModel.server.collectAsState() - val user by viewModel.user.collectAsState() - val pinned by viewModel.pinned.collectAsState() - - val backstack by navController.currentBackStack.collectAsState() - val canNavigateUp = backstack.size > 2 - LaunchedEffect(backstack) { - val backstackEntries = backstack.map { - it.destination.route - ?.replace("{server}", it.arguments?.getLong("server")?.toString() ?: "{server}") - ?.replace("{user}", it.arguments?.getLong("user")?.toString() ?: "{user}") - } - Log.i("Navigation", "BACKSTACK: [${backstackEntries.joinToString(" › ")}]") - } - - val avatarPainter = rememberAvatarPainter( - user?.gravatarUrl, - 32.dp, 32.dp, - MaterialTheme.colorScheme.primary - ) - - val iconPainter = painterResource(R.drawable.ic_launcher) - - Surface( - modifier = modifier - .padding(8.dp) - .height(64.dp), - color = MaterialTheme.colorScheme.surface, - shadowElevation = 6.dp, - tonalElevation = 6.dp, - shape = RoundedCornerShape(32.dp), - onClick = { navController.navigateUp() } - ) { - Row(modifier = Modifier.padding(8.dp)) { - if (user != null) { - Box( - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) - ) { - Image( - avatarPainter, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.align(Alignment.Center) - ) - } - } else if (canNavigateUp) { - Icon( - Icons.AutoMirrored.Default.ArrowBack, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), - modifier = Modifier - .padding(8.dp) - .size(32.dp) - ) - } else { - Image( - iconPainter, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .size(48.dp) - .clip(CircleShape) - ) - } - Spacer(Modifier.width(16.dp)) - Column( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1.0f, fill = true) - ) { - when { - user != null && server != null -> { - Text( - user!!.name, - fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - Text( - server!!.url.toHttpUrl().host, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), - fontWeight = FontWeight.Normal, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - } - - server != null && server!!.name != null -> { - Text( - server!!.name!!, - fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - Text( - server!!.url.toHttpUrl().host, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.67f), - fontWeight = FontWeight.Normal, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - } - - server != null -> { - Text( - server!!.url.toHttpUrl().host, - fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - } - - else -> { - Text( - "Meteroid", - fontWeight = FontWeight.SemiBold, - overflow = TextOverflow.Ellipsis, - softWrap = false - ) - } - } - } - Spacer(Modifier.width(16.dp)) - user?.let { user -> - IconButton(onClick = { - viewModel.togglePin(user.serverId, user.userId) - }) { - Icon( - if (pinned == true) Icons.Filled.PushPin - else Icons.Outlined.PushPin, - contentDescription = null - ) - } - Spacer(Modifier.width(8.dp)) - PriceBadge( - user.balance, - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(end = 12.dp) - ) - } - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt similarity index 52% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt index 2ff9789..9bc43f5 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/HomeSections.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAddServerItem.kt @@ -24,26 +24,33 @@ package de.chaosdorf.meteroid.ui.navigation +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.requiredHeight import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Celebration -import androidx.compose.material.icons.outlined.History -import androidx.compose.material.icons.outlined.LocalAtm -import androidx.compose.material.icons.twotone.Celebration -import androidx.compose.material.icons.twotone.History -import androidx.compose.material.icons.twotone.LocalAtm -import androidx.compose.ui.graphics.vector.ImageVector -import de.chaosdorf.meteroid.icons.MeteroidIcons -import de.chaosdorf.meteroid.icons.outlined.WaterFull -import de.chaosdorf.meteroid.icons.twotone.WaterFull +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.chaosdorf.meteroid.ui.AvatarLayout -enum class HomeSections( - override val title: String, - override val icon: ImageVector, - override val iconActive: ImageVector, - override val route: String -) : MeteroidNavSection { - PURCHASE("Purchase", MeteroidIcons.Outlined.WaterFull, MeteroidIcons.TwoTone.WaterFull, Routes.Home.Purchase), - DEPOSIT("Deposit", Icons.Outlined.LocalAtm, Icons.TwoTone.LocalAtm, Routes.Home.Deposit), - HISTORY("History", Icons.Outlined.History, Icons.TwoTone.History, Routes.Home.History), - WRAPPED("Wrapped", Icons.Outlined.Celebration, Icons.TwoTone.Celebration, Routes.Home.Wrapped); +@Composable +fun NavigationAddServerItem(expanded: Boolean, onExpand: () -> Unit, onClick: () -> Unit) { + val height: Dp by animateDpAsState(if (expanded) 48.dp else 64.dp, label = "height") + + ListItem( + headlineContent = { Text("Add Server") }, + leadingContent = { + AvatarLayout { + Icon(Icons.Default.Add, contentDescription = null) + } + }, + modifier = Modifier.requiredHeight(height) + .clickable(onClick = if (expanded) onClick else onExpand) + ) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt similarity index 75% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt index 218a3d8..d05bf86 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/MeteroidNavSection.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationAnimationContainer.kt @@ -24,19 +24,16 @@ package de.chaosdorf.meteroid.ui.navigation -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.runtime.Composable -interface MeteroidNavSection { - val title: String - val icon: ImageVector - val iconActive: ImageVector - val route: String - - fun withArguments(vararg args: Pair<String, String>): String { - var result = route - for ((parameter, value) in args) { - result = result.replace("{$parameter}", value) - } - return result +@Composable +fun NavigationAnimationContainer( + visible: Boolean, content: @Composable () -> Unit +) { + AnimatedVisibility(visible, enter = expandVertically(), exit = shrinkVertically()) { + content() } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt new file mode 100644 index 0000000..706d299 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationElement.kt @@ -0,0 +1,73 @@ +/* + * 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.navigation + +import de.chaosdorf.mete.model.ServerId +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.model.User +import de.chaosdorf.meteroid.ui.MeteroidScreen + +sealed class NavigationElement(val key: String) { + abstract fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean + + data class ServerElement( + val server: Server, + ) : NavigationElement("navigation-${server.serverId}") { + + override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean { + return route == MeteroidScreen.UserList && serverId == server.serverId + } + } + + data class UserElement( + val user: User, + val pinned: Boolean, + ) : NavigationElement("navigation-${user.serverId}-${user.userId}-${if (pinned) "pinned" else "current"}") { + override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean { + return route is MeteroidScreen.Home && serverId == user.serverId && userId == user.userId + } + } + + data class UserListElement( + val server: Server, + ) : NavigationElement("navigation-${server.serverId}-userList") { + override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean = false + } + + data object AddServerElement : NavigationElement("navigation-addServer") { + + override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean { + return route == MeteroidScreen.AddServer + } + } + + data object SettingsElement : NavigationElement("navigation-settings") { + + override fun isCurrent(route: MeteroidScreen?, serverId: ServerId?, userId: UserId?): Boolean { + return route == MeteroidScreen.Settings + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt new file mode 100644 index 0000000..265d989 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationScrim.kt @@ -0,0 +1,51 @@ +/* + * 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.navigation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier + +@Composable +fun NavigationScrim(viewModel: NavigationViewModel) { + val expanded by viewModel.expanded.collectAsState() + + AnimatedVisibility(expanded, enter = fadeIn(), exit = fadeOut()) { + Surface( + color = MaterialTheme.colorScheme.scrim.copy(alpha = 0.2f), + modifier = Modifier.fillMaxSize().clickable { + viewModel.expanded.value = false + } + ) {} + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt new file mode 100644 index 0000000..2092a71 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationServerItem.kt @@ -0,0 +1,55 @@ +/* + * 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.navigation + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import de.chaosdorf.meteroid.model.Server +import de.chaosdorf.meteroid.ui.ServerAvatar +import de.chaosdorf.meteroid.util.humanReadableHost +import okhttp3.HttpUrl.Companion.toHttpUrl + +@Composable +fun NavigationServerItem(expanded: Boolean, server: Server, onExpand: () -> Unit) { + val host = humanReadableHost(server.url.toHttpUrl()) + + ListItem( + headlineContent = { Text(server.name ?: host, maxLines = 1, overflow = TextOverflow.Ellipsis) }, + supportingContent = { if (server.name != null) Text(host, maxLines = 1, overflow = TextOverflow.Ellipsis) }, + leadingContent = { + ServerAvatar(server.logoUrl) + }, + modifier = Modifier.requiredHeight(64.dp).let { + if (expanded) it + else it.clickable(onClick = onExpand) + } + ) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt new file mode 100644 index 0000000..2034455 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationSettingsItem.kt @@ -0,0 +1,56 @@ +/* + * 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.navigation + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import de.chaosdorf.meteroid.ui.AvatarLayout + +@Composable +fun NavigationSettingsItem(expanded: Boolean, onExpand: () -> Unit, onClick: () -> Unit) { + val height: Dp by animateDpAsState(if (expanded) 48.dp else 64.dp, label = "height") + + ListItem( + headlineContent = { Text("Settings") }, + leadingContent = { + AvatarLayout { + Icon(Icons.Filled.Settings, contentDescription = null) + } + }, + modifier = Modifier.requiredHeight(height) + .clickable(onClick = if (expanded) onClick else onExpand) + ) +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt new file mode 100644 index 0000000..b42ba61 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserItem.kt @@ -0,0 +1,80 @@ +/* + * 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.navigation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material.icons.filled.StarOutline +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import de.chaosdorf.mete.model.ServerId +import de.chaosdorf.mete.model.UserId +import de.chaosdorf.meteroid.model.User +import de.chaosdorf.meteroid.ui.UserAvatar +import de.chaosdorf.meteroid.ui.home.PriceBadge + +@Composable +fun NavigationUserItem( + expanded: Boolean, + user: User, + pinned: Boolean, + onTogglePin: (ServerId, UserId) -> Unit, + onExpand: () -> Unit, + onClick: () -> Unit, +) { + ListItem( + headlineContent = { Text(user.name, maxLines = 1, overflow = TextOverflow.Ellipsis) }, + supportingContent = { if (user.email != null) Text(user.email!!, maxLines = 1, overflow = TextOverflow.Ellipsis) }, + leadingContent = { UserAvatar(user.gravatarUrl) }, + trailingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + AnimatedVisibility(!expanded, enter = fadeIn(), exit = fadeOut()) { + IconButton(onClick = { onTogglePin(user.serverId, user.userId) }) { + Icon( + if (pinned) Icons.Default.Star else Icons.Default.StarOutline, + contentDescription = null + ) + } + } + PriceBadge(user.balance) + } + }, + modifier = Modifier.requiredHeight(64.dp) + .clickable(onClick = if (expanded) onClick else onExpand) + ) +} 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 new file mode 100644 index 0000000..9a89652 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationUserListItem.kt @@ -0,0 +1,55 @@ +/* + * 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.navigation + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Group +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.chaosdorf.meteroid.ui.AvatarLayout + +@Composable +fun NavigationUserListItem(onClick: () -> Unit) { + Column { + ListItem( + headlineContent = { Text("All Users") }, + leadingContent = { + AvatarLayout { + Icon(Icons.Default.Group, contentDescription = null) + } + }, + modifier = Modifier.requiredHeight(48.dp).clickable(onClick = onClick) + ) + HorizontalDivider() + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt new file mode 100644 index 0000000..45d298c --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/NavigationViewModel.kt @@ -0,0 +1,120 @@ +/* + * 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.navigation + +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.* +import de.chaosdorf.meteroid.storage.AccountPreferences +import de.chaosdorf.meteroid.sync.AccountProvider +import de.chaosdorf.meteroid.sync.SyncManager +import de.chaosdorf.meteroid.sync.base.SyncHandler +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class NavigationViewModel @Inject constructor( + serverRepository: ServerRepository, + userRepository: UserRepository, + pinnedUserRepository: PinnedUserRepository, + syncManager: SyncManager, + private val accountProvider: AccountProvider +) : ViewModel() { + val expanded = MutableStateFlow(false) + val account = MutableStateFlow<AccountPreferences.State?>(null) + + val syncState = syncManager.syncState + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SyncHandler.State.Idle) + + private val servers: StateFlow<List<Server>> = serverRepository.getAllFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + private val pinnedUsers: StateFlow<List<User>> = pinnedUserRepository.getPinnedUsersFlow() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + private val currentUser: StateFlow<User?> = account.flatMapLatest { account -> + if (account?.server == null || account.user == null) flowOf(null) + else userRepository.getFlow(account.server, account.user) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) + + val historyDisabled = currentUser.map { + it?.audit == false + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + + val entries: StateFlow<List<NavigationElement>> = combine( + servers, + pinnedUsers, + currentUser, + ) { servers, pinnedUsers, currentUser -> + val entries = mutableListOf<NavigationElement>() + val userInList = currentUser != null && pinnedUsers.any { + it.serverId == currentUser.serverId && it.userId == currentUser.userId + } + + for (server in servers) { + entries.add(NavigationElement.ServerElement(server)) + if (currentUser != null && currentUser.serverId == server.serverId && !userInList) { + entries.add(NavigationElement.UserElement(currentUser, pinned = false)) + } + for (user in pinnedUsers) { + if (user.serverId == server.serverId) { + entries.add(NavigationElement.UserElement(user, pinned = true)) + } + } + entries.add(NavigationElement.UserListElement(server)) + } + + entries.add(NavigationElement.AddServerElement) + entries.add(NavigationElement.SettingsElement) + + entries + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + + init { + viewModelScope.launch { + account.collectLatest { account -> + val serverId = account?.server + val userId = account?.user + if (serverId != null && userId != null) { + val server = serverRepository.get(serverId) + val user = userRepository.get(serverId, userId) + if (server != null && user != null) { + syncManager.sync(server, user, incremental = true) + } + } + } + } + } + + fun togglePin(serverId: ServerId, userId: UserId) { + viewModelScope.launch { + accountProvider.togglePin(serverId, userId) + } + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt deleted file mode 100644 index 2d7fe45..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/navigation/Routes.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.navigation - -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.ServerId - -object Routes { - object Servers { - const val List = "server" - const val Add = "server/create" - } - object Users { - const val List = "server/{server}" - fun list(server: ServerId) = List - .replace("{server}", server.value.toString()) - } - object Home { - const val Purchase = "server/{server}/user/{user}/purchase" - const val Deposit = "server/{server}/user/{user}/deposit" - const val History = "server/{server}/user/{user}/history" - const val Wrapped = "server/{server}/user/{user}/wrapped" - fun purchase(server: ServerId, user: UserId) = Purchase - .replace("{server}", server.value.toString()) - .replace("{user}", user.value.toString()) - fun deposit(server: ServerId, user: UserId) = Deposit - .replace("{server}", server.value.toString()) - .replace("{user}", user.value.toString()) - fun history(server: ServerId, user: UserId) = History - .replace("{server}", server.value.toString()) - .replace("{user}", user.value.toString()) - fun wrapped(server: ServerId, user: UserId) = Wrapped - .replace("{server}", server.value.toString()) - .replace("{user}", user.value.toString()) - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt index f5ea039..a629d0b 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerScreen.kt @@ -24,22 +24,10 @@ package de.chaosdorf.meteroid.ui.servers -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.Card -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextField +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -51,7 +39,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController import coil.compose.AsyncImage -import de.chaosdorf.meteroid.ui.navigation.Routes import kotlinx.coroutines.launch import okhttp3.HttpUrl.Companion.toHttpUrl @@ -112,7 +99,7 @@ fun AddServerScreen( IconButton(onClick = { scope.launch { viewModel.addServer() - navController.navigate(Routes.Servers.List) + navController.navigateUp() } }) { Icon(Icons.Default.Add, contentDescription = "Add Server") diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt index 26100e6..8c1ee10 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/AddServerViewModel.kt @@ -28,16 +28,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.ServerId import de.chaosdorf.meteroid.model.ServerRepository import de.chaosdorf.meteroid.util.newServer -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 javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt deleted file mode 100644 index 19242ea..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/servers/ServerListScreen.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.layout.PaddingValues -import androidx.compose.foundation.layout.size -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.layout.ContentScale -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import coil.compose.AsyncImage -import de.chaosdorf.meteroid.ui.navigation.Routes -import okhttp3.HttpUrl.Companion.toHttpUrl - -@Composable -fun ServerListScreen( - navController: NavController, - viewModel: ServerListViewModel, - contentPadding: PaddingValues = PaddingValues(), -) { - val servers by viewModel.servers.collectAsState() - - LazyColumn(contentPadding = contentPadding) { - items(servers) { server -> - ListItem( - headlineContent = { Text(server.name ?: server.url) }, - supportingContent = { if (server.name != null) Text(server.url.toHttpUrl().host) }, - leadingContent = { - AsyncImage( - server.logoUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier.size(48.dp) - ) - }, - modifier = Modifier.clickable { - navController.navigate(Routes.Users.list(server.serverId)) - viewModel.selectServer(server.serverId) - } - ) - } - item { - ListItem( - headlineContent = { Text("Add Server") }, - modifier = Modifier.clickable { - navController.navigate(Routes.Servers.Add) - } - ) - } - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..898ff67 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsScreen.kt @@ -0,0 +1,45 @@ +/* + * 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.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController + +@Composable +fun SettingsScreen(navController: NavController, viewModel: SettingsViewModel, contentPadding: PaddingValues) { + Column( + Modifier + .padding(contentPadding) + .padding(16.dp, 8.dp) + ) { + Text("Settings") + } +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt similarity index 92% rename from app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt index 5ae0c58..35aa5a2 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sync/SyncViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/settings/SettingsViewModel.kt @@ -22,14 +22,11 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.sync +package de.chaosdorf.meteroid.ui.settings import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class SyncViewModel @Inject constructor( -) : ViewModel() { - -} +class SettingsViewModel @Inject constructor() : ViewModel() diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt similarity index 97% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt index ba11f91..f903e30 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListItem.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListItem.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.users +package de.chaosdorf.meteroid.ui.userlist import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -39,8 +39,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.ServerId import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.util.rememberAvatarPainter diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt similarity index 79% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt index ef1c017..a7e6156 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListScreen.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListScreen.kt @@ -22,7 +22,7 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.users +package de.chaosdorf.meteroid.ui.userlist import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.PaddingValues @@ -38,7 +38,8 @@ 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.navigation.Routes +import de.chaosdorf.meteroid.ui.MeteroidScreen +import de.chaosdorf.meteroid.util.findStartDestination @OptIn(ExperimentalFoundationApi::class) @Composable @@ -64,10 +65,16 @@ fun UserListScreen( items( pinnedUsers, - key = { "pinned-${it.userId}" }, + key = { "pinned-${it.serverId}-${it.userId}" }, ) { user -> UserListItem(user) { serverId, userId -> - navController.navigate(Routes.Home.purchase(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) } } @@ -88,10 +95,16 @@ fun UserListScreen( items( group, - key = { "${it.serverId}-${it.userId}" }, + key = { "user-${it.serverId}-${it.userId}" }, ) { user -> UserListItem(user) { serverId, userId -> - navController.navigate(Routes.Home.purchase(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) } } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt similarity index 97% rename from app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt index 0823feb..22a6a09 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserListViewModel.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/userlist/UserListViewModel.kt @@ -22,16 +22,16 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.ui.users +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.ServerId import de.chaosdorf.meteroid.model.User import de.chaosdorf.meteroid.model.UserRepository import de.chaosdorf.meteroid.storage.AccountPreferences diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt deleted file mode 100644 index 16a5f7c..0000000 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/ui/users/UserTile.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.users - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.paddingFromBaseline -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import coil.compose.rememberAsyncImagePainter -import de.chaosdorf.mete.model.UserId -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.User -import de.chaosdorf.meteroid.sample.SampleDrinkProvider -import de.chaosdorf.meteroid.ui.PriceBadge - -@Composable -fun UserTile( - item: User, - onSelect: (UserId) -> Unit = {} -) { - val avatarPainter = rememberAsyncImagePainter( - item.gravatarUrl - ) - - Column( - modifier = Modifier - .height(IntrinsicSize.Max) - .alpha(if (item.active) 1.0f else 0.67f) - .clip(RoundedCornerShape(8.dp)) - .clickable { onSelect(item.userId) } - .padding(8.dp) - ) { - Box { - Image( - avatarPainter, - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .aspectRatio(1.0f) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) - ) - } - Spacer(Modifier.height(4.dp)) - Text( - item.name, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.SemiBold, - style = MaterialTheme.typography.labelLarge, - ) - } -} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/BundleExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/BundleExtensions.kt new file mode 100644 index 0000000..902e0c5 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/BundleExtensions.kt @@ -0,0 +1,30 @@ +/* + * 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.util + +import android.os.Bundle + +internal fun Bundle.toMap(): Map<String, Any?> = + keySet().associate { @Suppress("DEPRECATION") Pair(it, get(it)) } diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/HumanReadableHost.kt similarity index 54% rename from app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt rename to app/src/main/kotlin/de/chaosdorf/meteroid/util/HumanReadableHost.kt index 690b73c..6379d56 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/sample/SampleDrinkProvider.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/HumanReadableHost.kt @@ -22,34 +22,14 @@ * THE SOFTWARE. */ -package de.chaosdorf.meteroid.sample +package de.chaosdorf.meteroid.util -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import de.chaosdorf.mete.model.DrinkId -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.ServerId +import okhttp3.HttpUrl -class SampleDrinkProvider : PreviewParameterProvider<Drink> { - override val values = sequenceOf( - Drink( - serverId = ServerId(-1), - drinkId = DrinkId(27), - active = true, - name = "Club Mate", - volume = 0.5.toBigDecimal(), - caffeine = null, - price = 1.5.toBigDecimal(), - logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/027/thumb/logo.png", - ), - Drink( - serverId = ServerId(-1), - drinkId = DrinkId(15), - active = false, - name = "Paulaner Spezi", - volume = 0.5.toBigDecimal(), - caffeine = null, - price = 1.5.toBigDecimal(), - logoUrl = "http://192.168.188.36:8080/system/drinks/logos/000/000/015/thumb/logo.png", - ) - ) +fun humanReadableHost(url: HttpUrl): String { + val actualPort = url.port + val defaultPort = HttpUrl.defaultPort(url.scheme) + val actualHost = url.host + if (actualPort == defaultPort) return actualHost + return "$actualHost:$actualPort" } 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 a9b65cf..e8371e2 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/MeteApiFactoryExtensions.kt @@ -25,8 +25,8 @@ package de.chaosdorf.meteroid.util import de.chaosdorf.mete.model.MeteApiFactory +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerId import java.net.URI suspend fun MeteApiFactory.newServer(serverId: ServerId, baseUrl: String): Server? = try { diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackEntryExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackEntryExtensions.kt new file mode 100644 index 0000000..64ac766 --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavBackStackEntryExtensions.kt @@ -0,0 +1,42 @@ +/* + * 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.util + +import androidx.navigation.NavBackStackEntry + +internal fun NavBackStackEntry.toFancyString(): String { + val arguments = this.arguments?.toMap().orEmpty().toList() + .filter { (key, _) -> key == "server" || key == "user" } + .joinToString(", ", prefix = "(", postfix = ")") { (key, value) -> "$key=$value" } + + return "${destination.route}$arguments" +} + +internal fun Iterable<NavBackStackEntry>.toFancyString(): String = joinToString( + separator = " › ", + prefix = "[", + postfix = "]", + transform = NavBackStackEntry::toFancyString +) diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavGraphExtensions.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavGraphExtensions.kt new file mode 100644 index 0000000..bac65da --- /dev/null +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/NavGraphExtensions.kt @@ -0,0 +1,35 @@ +/* + * 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.util + +import androidx.navigation.NavDestination +import androidx.navigation.NavGraph + +private val NavGraph.startDestination: NavDestination? + get() = findNode(startDestinationId) + +tailrec fun findStartDestination(graph: NavDestination): NavDestination { + return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph +} diff --git a/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt b/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt index d1f83fa..463fdf4 100644 --- a/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt +++ b/app/src/main/kotlin/de/chaosdorf/meteroid/util/RememberAvatarPainter.kt @@ -37,15 +37,15 @@ import coil.compose.rememberAsyncImagePainter @Composable fun rememberAvatarPainter(url: String?, iconWidth: Dp, iconHeight: Dp, iconTint: Color): AsyncImagePainter { val personPainter = rememberVectorPainter( - defaultHeight = iconHeight, - defaultWidth = iconWidth, - viewportWidth = Icons.Filled.Person.viewportWidth, - viewportHeight = Icons.Filled.Person.viewportHeight, - name = Icons.Filled.Person.name, - tintColor = iconTint, - tintBlendMode = Icons.Filled.Person.tintBlendMode, - autoMirror = Icons.Filled.Person.autoMirror, - content = { _, _ -> RenderVectorGroup(group = Icons.Filled.Person.root) } + defaultHeight = iconHeight, + defaultWidth = iconWidth, + viewportWidth = Icons.Filled.Person.viewportWidth, + viewportHeight = Icons.Filled.Person.viewportHeight, + name = Icons.Filled.Person.name, + tintColor = iconTint, + tintBlendMode = Icons.Filled.Person.tintBlendMode, + autoMirror = Icons.Filled.Person.autoMirror, + content = { _, _ -> RenderVectorGroup(group = Icons.Filled.Person.root) } ) return rememberAsyncImagePainter(url, fallback = personPainter) } diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher.xml index 2e4644c..b45acc3 100644 --- a/app/src/main/res/drawable/ic_launcher.xml +++ b/app/src/main/res/drawable/ic_launcher.xml @@ -23,20 +23,21 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:aapt="http://schemas.android.com/aapt" - xmlns:tools="http://schemas.android.com/tools" - android:width="72dp" - android:height="72dp" - android:viewportWidth="72" - android:viewportHeight="72"> + xmlns:aapt="http://schemas.android.com/aapt" + xmlns:tools="http://schemas.android.com/tools" + android:width="72dp" + android:height="72dp" + android:viewportWidth="72" + android:viewportHeight="72"> <path android:fillColor="#003984" - android:pathData="m11.5 0c-6.35 0-11.5 5.15-11.5 11.5v49c0 6.35 5.15 11.5 11.5 11.5h49c6.35 0 11.5-5.15 11.5-11.5v-49c0-6.35-5.15-11.5-11.5-11.5z" /> + android:pathData="m11.5 0c-6.35 0-11.5 5.15-11.5 11.5v49c0 6.35 5.15 11.5 11.5 11.5h49c6.35 0 11.5-5.15 11.5-11.5v-49c0-6.35-5.15-11.5-11.5-11.5z"/> <path android:fillAlpha="0.5" android:fillColor="#000000" - android:pathData="m21.7 60.9v11.1h38.8c0 0 0.256-0 0-0l-10.6-11.1z" /> - <path android:pathData="m53.5 23.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z"> + android:pathData="m21.7 60.9v11.1h38.8c0 0 0.256-0 0-0l-10.6-11.1z"/> + <path + android:pathData="m53.5 23.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z"> <aapt:attr name="android:fillColor"> <gradient android:endX="0" @@ -46,22 +47,22 @@ android:type="linear"> <item android:color="#ff9800" - android:offset="0.6" /> + android:offset="0.6"/> <item android:color="#f9c579" - android:offset="1.0" /> + android:offset="1.0"/> </gradient> </aapt:attr> </path> <path android:fillColor="#bad9ff" android:pathData="m36 15.4c-5.15 0-9.85 0.237-13.3 0.625-1.71 0.194-3.09 0.422-4.09 0.69-0.5 0.135-0.9 0.273-1.24 0.469-0.17 0.098-0.329 0.21-0.474 0.39-0.144 0.18-0.268 0.475-0.232 0.77l5 42.2c0.072 0.61 0.53 1.08 1.12 1.4 0.59 0.314 1.38 0.545 2.46 0.745 2.17 0.398 5.55 0.63 10.7 0.63s8.55-0.233 10.7-0.63c1.08-0.199 1.87-0.432 2.46-0.745 0.59-0.314 1.05-0.785 1.12-1.39l5-42.2c0.0353-0.298-0.088-0.59-0.232-0.77s-0.305-0.292-0.474-0.39c-0.339-0.196-0.74-0.334-1.24-0.469-1-0.27-2.38-0.499-4.09-0.69-3.42-0.387-8.1-0.625-13.3-0.625zm0 1.5c5.1 0 9.8 0.238 13.1 0.615 1.66 0.188 3 0.415 3.87 0.65 0.398 0.108 0.665 0.218 0.815 0.297l-4.98 42c0.0034-0.0282 0.021 0.0545-0.337 0.245-0.358 0.19-1.02 0.412-2.02 0.595-2 0.366-5.35 0.605-10.4 0.605s-8.45-0.24-10.4-0.605c-1-0.183-1.66-0.404-2.02-0.595-0.358-0.19-0.34-0.274-0.337-0.245l-4.98-42c0.15-0.0785 0.416-0.189 0.815-0.297 0.865-0.234 2.2-0.46 3.87-0.65 3.33-0.377 8-0.615 13.1-0.615z" - tools:ignore="VectorPath" /> + tools:ignore="VectorPath"/> <path android:fillColor="#ffe2b7" android:pathData="m27 26.4a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm18 4.49a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm-19.4 6.2a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm3.92 3.85a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 0 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm12.2 0.725a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-4.46 4.02a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-6.35 1.4a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm11.6 1.92a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-5.15 1.54a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.48a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm10.4 3.06a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.132a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm3.72 1.64a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm2.63 2.34a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845z" - tools:ignore="VectorPath" /> + tools:ignore="VectorPath"/> <path android:fillColor="#d91616" - android:pathData="m36 26.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z" /> + android:pathData="m36 26.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z"/> </vector> diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index b663e5e..df5ce8b 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -23,17 +23,18 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:aapt="http://schemas.android.com/aapt" - xmlns:tools="http://schemas.android.com/tools" - android:width="108dp" - android:height="108dp" - android:viewportWidth="108" - android:viewportHeight="108"> + xmlns:aapt="http://schemas.android.com/aapt" + xmlns:tools="http://schemas.android.com/tools" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> <path android:fillAlpha="0.5" android:fillColor="#000000" - android:pathData="m39.7 78.9h28.6l28 29.1h-56.5z" /> - <path android:pathData="m71.5 41.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z"> + android:pathData="m39.7 78.9h28.6l28 29.1h-56.5z"/> + <path + android:pathData="m71.5 41.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z"> <aapt:attr name="android:fillColor"> <gradient android:endX="0" @@ -43,22 +44,22 @@ android:type="linear"> <item android:color="#ff9800" - android:offset="0.6" /> + android:offset="0.6"/> <item android:color="#f9c579" - android:offset="1.0" /> + android:offset="1.0"/> </gradient> </aapt:attr> </path> <path android:fillColor="#bad9ff" android:pathData="m54 33.4c-5.15 0-9.85 0.237-13.3 0.625-1.71 0.194-3.09 0.422-4.09 0.69-0.5 0.135-0.9 0.273-1.24 0.469-0.17 0.098-0.329 0.21-0.474 0.39-0.144 0.18-0.268 0.475-0.232 0.77l5 42.2c0.072 0.61 0.53 1.08 1.12 1.4 0.59 0.314 1.38 0.545 2.46 0.745 2.17 0.398 5.55 0.63 10.7 0.63s8.55-0.233 10.7-0.63c1.08-0.199 1.87-0.432 2.46-0.745 0.59-0.314 1.05-0.785 1.12-1.39l5-42.2c0.0353-0.298-0.088-0.59-0.232-0.77s-0.305-0.292-0.474-0.39c-0.339-0.196-0.74-0.334-1.24-0.469-1-0.27-2.38-0.499-4.09-0.69-3.42-0.387-8.1-0.625-13.3-0.625zm0 1.5c5.1 0 9.8 0.238 13.1 0.615 1.66 0.188 3 0.415 3.87 0.65 0.398 0.108 0.665 0.218 0.815 0.297l-4.98 42c0.0034-0.0282 0.021 0.0545-0.337 0.245-0.358 0.19-1.02 0.412-2.02 0.595-2 0.366-5.35 0.605-10.4 0.605s-8.45-0.24-10.4-0.605c-1-0.183-1.66-0.404-2.02-0.595-0.358-0.19-0.34-0.274-0.337-0.245l-4.98-42c0.15-0.0785 0.416-0.189 0.815-0.297 0.865-0.234 2.2-0.46 3.87-0.65 3.33-0.377 8-0.615 13.1-0.615z" - tools:ignore="VectorPath" /> + tools:ignore="VectorPath"/> <path android:fillColor="#ffe2b7" android:pathData="m45 44.4a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm18 4.49a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm-19.4 6.2a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm3.92 3.85a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 0 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm12.2 0.725a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-4.46 4.02a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-6.35 1.4a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm11.6 1.92a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-5.15 1.54a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.48a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm10.4 3.06a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.132a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm3.72 1.64a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm2.63 2.34a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845z" - tools:ignore="VectorPath" /> + tools:ignore="VectorPath"/> <path android:fillColor="#d91616" - android:pathData="m54 44.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z" /> + android:pathData="m54 44.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z"/> </vector> diff --git a/app/src/main/res/drawable/ic_splash.xml b/app/src/main/res/drawable/ic_splash.xml index 17bb99f..ea9f9bc 100644 --- a/app/src/main/res/drawable/ic_splash.xml +++ b/app/src/main/res/drawable/ic_splash.xml @@ -23,13 +23,14 @@ --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:aapt="http://schemas.android.com/aapt" - xmlns:tools="http://schemas.android.com/tools" - android:width="108dp" - android:height="108dp" - android:viewportWidth="108" - android:viewportHeight="108"> - <path android:pathData="m71.5 41.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z"> + xmlns:aapt="http://schemas.android.com/aapt" + xmlns:tools="http://schemas.android.com/tools" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:pathData="m71.5 41.5-4.46 37.2c-0.136 1.14-3.2 1.6-13.3 1.6s-13.2-0.465-13.3-1.6l-4.38-39c14.3 4.73 19.3-2.75 35.4 1.86z"> <aapt:attr name="android:fillColor"> <gradient android:endX="0" @@ -39,22 +40,22 @@ android:type="linear"> <item android:color="#ff9800" - android:offset="0.6" /> + android:offset="0.6"/> <item android:color="#f9c579" - android:offset="1.0" /> + android:offset="1.0"/> </gradient> </aapt:attr> </path> <path android:fillColor="#bad9ff" android:pathData="m54 33.4c-5.15 0-9.85 0.237-13.3 0.625-1.71 0.194-3.09 0.422-4.09 0.69-0.5 0.135-0.9 0.273-1.24 0.469-0.17 0.098-0.329 0.21-0.474 0.39-0.144 0.18-0.268 0.475-0.232 0.77l5 42.2c0.072 0.61 0.53 1.08 1.12 1.4 0.59 0.314 1.38 0.545 2.46 0.745 2.17 0.398 5.55 0.63 10.7 0.63s8.55-0.233 10.7-0.63c1.08-0.199 1.87-0.432 2.46-0.745 0.59-0.314 1.05-0.785 1.12-1.39l5-42.2c0.0353-0.298-0.088-0.59-0.232-0.77s-0.305-0.292-0.474-0.39c-0.339-0.196-0.74-0.334-1.24-0.469-1-0.27-2.38-0.499-4.09-0.69-3.42-0.387-8.1-0.625-13.3-0.625zm0 1.5c5.1 0 9.8 0.238 13.1 0.615 1.66 0.188 3 0.415 3.87 0.65 0.398 0.108 0.665 0.218 0.815 0.297l-4.98 42c0.0034-0.0282 0.021 0.0545-0.337 0.245-0.358 0.19-1.02 0.412-2.02 0.595-2 0.366-5.35 0.605-10.4 0.605s-8.45-0.24-10.4-0.605c-1-0.183-1.66-0.404-2.02-0.595-0.358-0.19-0.34-0.274-0.337-0.245l-4.98-42c0.15-0.0785 0.416-0.189 0.815-0.297 0.865-0.234 2.2-0.46 3.87-0.65 3.33-0.377 8-0.615 13.1-0.615z" - tools:ignore="VectorPath" /> + tools:ignore="VectorPath"/> <path android:fillColor="#ffe2b7" android:pathData="m45 44.4a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm18 4.49a2.96 2.96 0 0 0-2.74 2.96 2.98 2.98 0 0 0 5.95 0 2.96 2.96 0 0 0-3.18-2.96zm-19.4 6.2a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm3.92 3.85a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 0 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm12.2 0.725a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-4.46 4.02a1.7 1.7 0 0 0-1.56 1.69 1.7 1.7 0 1 0 3.39 0 1.7 1.7 0 0 0-1.82-1.69zm-6.35 1.4a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm11.6 1.92a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-5.15 1.54a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.48a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm10.4 3.06a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm-7.75 0.132a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 1 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm3.72 1.64a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845zm2.63 2.34a0.845 0.845 0 0 0-0.785 0.845 0.848 0.848 0 0 0 1.7 0 0.845 0.845 0 0 0-0.91-0.845z" - tools:ignore="VectorPath" /> + tools:ignore="VectorPath"/> <path android:fillColor="#d91616" - android:pathData="m54 44.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z" /> + android:pathData="m54 44.3 3.06 6.45 7.05 0.92-5.15 4.9 1.3 7-6.25-3.4-6.25 3.4 1.3-7-5.15-4.9 7.05-0.92z"/> </vector> diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd..5f349f7 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@color/ic_launcher_background"/> - <foreground android:drawable="@drawable/ic_launcher_foreground"/> -</adaptive-icon> \ No newline at end of file + <background android:drawable="@color/ic_launcher_background"/> + <foreground android:drawable="@drawable/ic_launcher_foreground"/> +</adaptive-icon> diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd..5f349f7 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> - <background android:drawable="@color/ic_launcher_background"/> - <foreground android:drawable="@drawable/ic_launcher_foreground"/> -</adaptive-icon> \ No newline at end of file + <background android:drawable="@color/ic_launcher_background"/> + <foreground android:drawable="@drawable/ic_launcher_foreground"/> +</adaptive-icon> diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml index 4e4cd80..c47f2aa 100644 --- a/app/src/main/res/values/ic_launcher_background.xml +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <color name="ic_launcher_background">#003984</color> -</resources> \ No newline at end of file + <color name="ic_launcher_background">#003984</color> +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be7b46a..4e1d485 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,5 +23,5 @@ --> <resources> - <string name="application_name">Meteroid</string> + <string name="application_name">Meteroid</string> </resources> diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 37bf28c..5cb87d3 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -9,7 +9,7 @@ <item name="android:dialogTheme">@style/Theme.DialogFullScreen</item> </style> - <style name="Theme.Material.DayNight.NoActionBar" parent="@android:style/Theme.Material.Light.NoActionBar" /> + <style name="Theme.Material.DayNight.NoActionBar" parent="@android:style/Theme.Material.Light.NoActionBar"/> <style name="Theme.DialogFullScreen" parent="Theme.Material.DayNight.NoActionBar"> <item name="android:windowMinWidthMajor">100%</item> diff --git a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt index a6a58c4..17c95bd 100644 --- a/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt +++ b/gradle/convention/src/main/kotlin/AndroidApplicationConvention.kt @@ -5,7 +5,7 @@ import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import util.cmd import util.properties -import java.util.Locale +import java.util.* class AndroidApplicationConvention : Plugin<Project> { override fun apply(target: Project) { diff --git a/gradle/convention/src/main/kotlin/KotlinConvention.kt b/gradle/convention/src/main/kotlin/KotlinConvention.kt index d99f881..ce81fe9 100644 --- a/gradle/convention/src/main/kotlin/KotlinConvention.kt +++ b/gradle/convention/src/main/kotlin/KotlinConvention.kt @@ -1,4 +1,3 @@ -import org.gradle.api.JavaVersion import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginExtension @@ -7,9 +6,7 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension -import org.jetbrains.kotlin.gradle.dsl.kotlinExtension class KotlinConvention : Plugin<Project> { override fun apply(target: Project) { diff --git a/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt b/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt index 9ab40fe..d1d4eb7 100644 --- a/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt +++ b/gradle/convention/src/main/kotlin/util/ProjectExtensions.kt @@ -2,7 +2,7 @@ package util import org.gradle.api.Project import java.io.ByteArrayOutputStream -import java.util.Properties +import java.util.* fun Project.cmd(vararg command: String) = try { val stdOut = ByteArrayOutputStream() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da77120..b20ab82 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,12 +2,10 @@ androidGradlePlugin = "8.1.4" androidx-activity = "1.8.1" androidx-appcompat = "1.6.1" -androidx-compose-bom = "2023.10.01" +androidx-compose = "1.6.0-beta02" androidx-compose-compiler = "1.5.5" -androidx-compose-material = "1.5.0-alpha04" androidx-compose-material3 = "1.2.0-alpha12" androidx-compose-runtimetracing = "1.0.0-beta01" -androidx-compose-tooling = "1.6.0-beta02" androidx-datastore = "1.0.0" androidx-hilt = "1.1.0" androidx-navigation = "2.7.5" @@ -30,21 +28,20 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a 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-bom = { group = "androidx.compose", name = "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-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } -androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.6.0-beta01" } +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-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "androidx-compose" } +androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version = "androidx-compose" } androidx-compose-material = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } androidx-compose-material-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "androidx-compose-material3" } -androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } -androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } +androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidx-compose" } +androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "androidx-compose" } androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidx-compose-runtimetracing" } -androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test" } -androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose-tooling" } -androidx-compose-ui-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose-tooling" } -androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" } +androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test", version.ref = "androidx-compose" } +androidx-compose-ui-testManifest = { group = "androidx.compose.ui", name = "ui-test-manifest", 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-ui-util = { group = "androidx.compose.ui", name = "ui-util", version.ref = "androidx-compose" } androidx-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" } @@ -91,4 +88,4 @@ android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger-hilt" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin.ksp" } +kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" } diff --git a/lint.xml b/lint.xml index eab67bd..cbbcf6b 100644 --- a/lint.xml +++ b/lint.xml @@ -18,36 +18,36 @@ --> <lint> - <issue id="NewerVersionAvailable" severity="error" /> + <issue id="NewerVersionAvailable" severity="error"/> <!-- Because of course paging and room have incompatible versions --> - <issue id="GradleCompatible" severity="ignore" /> + <issue id="GradleCompatible" severity="ignore"/> <!-- Because these are entirely broken --> - <issue id="ResourceType" severity="ignore" /> - <issue id="UnusedResources" severity="ignore" /> - <issue id="ObsoleteLintCustomCheck" severity="ignore" /> - <issue id="UnusedAttribute" severity="informational" /> + <issue id="ResourceType" severity="ignore"/> + <issue id="UnusedResources" severity="ignore"/> + <issue id="ObsoleteLintCustomCheck" severity="ignore"/> + <issue id="UnusedAttribute" severity="informational"/> <!-- Because this doesn’t work when using splash themes --> - <issue id="Overdraw" severity="ignore" /> + <issue id="Overdraw" severity="ignore"/> <!-- Can’t request a translation without a release, can’t release without translation --> - <issue id="MissingTranslation" severity="informational" /> + <issue id="MissingTranslation" severity="informational"/> <!-- Because we don’t use app bundles and never will use them --> - <issue id="AppBundleLocaleChanges" severity="informational" /> + <issue id="AppBundleLocaleChanges" severity="informational"/> <!-- Because this tries to apply english orthography to other locales --> - <issue id="Typos" severity="ignore" /> + <issue id="Typos" severity="ignore"/> <!-- Because Autofill isn’t a priority at the moment --> - <issue id="Autofill" severity="informational" /> + <issue id="Autofill" severity="informational"/> <!-- We’re not using AGP 5 yet, and once we are, we’ll use compose anyway --> - <issue id="NonConstantResourceId" severity="informational" /> + <issue id="NonConstantResourceId" severity="informational"/> <!-- It’s only used for testing --> - <issue id="TrustAllX509TrustManager" severity="informational" /> + <issue id="TrustAllX509TrustManager" severity="informational"/> <!-- TODO for the future --> - <issue id="DataExtractionRules" severity="informational" /> + <issue id="DataExtractionRules" severity="informational"/> </lint> diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt index 63251ae..b82df85 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/MeteroidDatabase.kt @@ -24,20 +24,10 @@ package de.chaosdorf.meteroid -import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters -import de.chaosdorf.meteroid.model.Drink -import de.chaosdorf.meteroid.model.DrinkRepository -import de.chaosdorf.meteroid.model.PinnedUser -import de.chaosdorf.meteroid.model.PinnedUserRepository -import de.chaosdorf.meteroid.model.Server -import de.chaosdorf.meteroid.model.ServerRepository -import de.chaosdorf.meteroid.model.Transaction -import de.chaosdorf.meteroid.model.TransactionRepository -import de.chaosdorf.meteroid.model.User -import de.chaosdorf.meteroid.model.UserRepository +import de.chaosdorf.meteroid.model.* import de.chaosdorf.meteroid.util.BigDecimalTypeConverter import de.chaosdorf.meteroid.util.KotlinDatetimeTypeConverter diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt index 18d5a79..97593d1 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Drink.kt @@ -24,14 +24,10 @@ package de.chaosdorf.meteroid.model -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* import de.chaosdorf.mete.model.DrinkId import de.chaosdorf.mete.model.DrinkModel +import de.chaosdorf.mete.model.ServerId import kotlinx.coroutines.flow.Flow import java.math.BigDecimal import java.net.URI diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt index 455a90c..8478850 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/PinnedUser.kt @@ -24,12 +24,8 @@ package de.chaosdorf.meteroid.model -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId import kotlinx.coroutines.flow.Flow 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 0e94a9b..5537758 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Server.kt @@ -24,17 +24,10 @@ package de.chaosdorf.meteroid.model -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.PrimaryKey -import androidx.room.Query +import androidx.room.* +import de.chaosdorf.mete.model.ServerId import kotlinx.coroutines.flow.Flow -@JvmInline -value class ServerId(val value: Long) - @Entity data class Server( @PrimaryKey @@ -51,6 +44,7 @@ interface ServerRepository { @Query("SELECT * FROM Server WHERE serverId = :id LIMIT 1") fun getFlow(id: ServerId): Flow<Server?> + @Query("SELECT count(*) FROM Server") fun countFlow(): Flow<Int> diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt index f320689..66da965 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/Transaction.kt @@ -24,17 +24,8 @@ package de.chaosdorf.meteroid.model -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import de.chaosdorf.mete.model.DrinkId -import de.chaosdorf.mete.model.TransactionId -import de.chaosdorf.mete.model.TransactionModel -import de.chaosdorf.mete.model.UserId +import androidx.room.* +import de.chaosdorf.mete.model.* import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant import java.math.BigDecimal @@ -86,6 +77,7 @@ interface TransactionRepository { @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC") fun getAllFlow(serverId: ServerId, userId: UserId): Flow<List<Transaction>> + @Query("SELECT * FROM `Transaction` WHERE serverId = :serverId AND userId = :userId ORDER BY timestamp DESC LIMIT 1") suspend fun getLatest(serverId: ServerId, userId: UserId): Transaction? diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt index 8052cde..dd0c94f 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/model/User.kt @@ -24,18 +24,13 @@ package de.chaosdorf.meteroid.model -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* +import de.chaosdorf.mete.model.ServerId import de.chaosdorf.mete.model.UserId import de.chaosdorf.mete.model.UserModel +import de.chaosdorf.meteroid.util.gravatarUrl import kotlinx.coroutines.flow.Flow import java.math.BigDecimal -import java.security.MessageDigest -import java.util.Locale @Entity( primaryKeys = ["serverId", "userId"], @@ -53,16 +48,7 @@ data class User( val audit: Boolean, val redirect: Boolean, ) { - @OptIn(ExperimentalStdlibApi::class) - val gravatarUrl: String? by lazy { - email?.let { - val normalised: String = it.lowercase(Locale.ROOT) - val binaryData: ByteArray = normalised.toByteArray(Charsets.UTF_8) - val binaryHash: ByteArray = MessageDigest.getInstance("MD5").digest(binaryData) - val hash: String = binaryHash.toHexString() - "https://www.gravatar.com/avatar/$hash?d=404&s=640" - } - } + val gravatarUrl: String? by lazy { email?.let(::gravatarUrl) } companion object { fun fromModel(server: Server, value: UserModel) = User( @@ -86,6 +72,12 @@ interface UserRepository { @Query("SELECT * FROM User WHERE serverId = :serverId AND userId = :userId LIMIT 1") fun getFlow(serverId: ServerId, userId: UserId): Flow<User?> + @Query("SELECT * FROM User ORDER BY NAME ASC") + suspend fun getAll(): List<User> + + @Query("SELECT * FROM User ORDER BY NAME ASC") + fun getAllFlow(): Flow<List<User>> + @Query("SELECT * FROM User WHERE serverId = :serverId ORDER BY NAME ASC") suspend fun getAll(serverId: ServerId): List<User> diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/GravatarUrl.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/GravatarUrl.kt new file mode 100644 index 0000000..05700b4 --- /dev/null +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/GravatarUrl.kt @@ -0,0 +1,60 @@ +/* + * 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.util + +import java.security.MessageDigest +import java.util.* + +enum class GravatarFallbackType(val tag: String) { + FileNotFound("404"), + MysteryPerson("mp"), + Identicon("identicon"), + MonsterId("monsterid"), + Wavatar("wavatar"), + Retro("retro"), + Robohash("robohash"), + Blank("blank") +} + +enum class GravatarRating(val tag: String) { + General("g"), + ParentalGuidance("pg"), + Restricted("r"), + Adult("x") +} + +@OptIn(ExperimentalStdlibApi::class) +fun gravatarUrl( + email: String, + size: Int = 640, + fallback: GravatarFallbackType = GravatarFallbackType.FileNotFound, + rating: GravatarRating = GravatarRating.General, +): String { + val normalised: String = email.trim().lowercase(Locale.ROOT) + val binaryData: ByteArray = normalised.toByteArray(Charsets.UTF_8) + val binaryHash: ByteArray = MessageDigest.getInstance("SHA-256").digest(binaryData) + val hash: String = binaryHash.toHexString() + return "https://gravatar.com/avatar/$hash?default=${fallback.tag}&size=$size&rating=${rating.tag}" +} diff --git a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt index 289ea21..b39982e 100644 --- a/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt +++ b/persistence/src/main/kotlin/de/chaosdorf/meteroid/util/KotlinDatetimeTypeConverter.kt @@ -30,6 +30,7 @@ import kotlinx.datetime.Instant class KotlinDatetimeTypeConverter { @TypeConverter fun load(value: Long): Instant = Instant.fromEpochMilliseconds(value) + @TypeConverter fun store(value: Instant): Long = value.toEpochMilliseconds() } -- GitLab