diff --git a/.github/workflows/checkStyle.yml b/.github/workflows/checkStyle.yml new file mode 100644 index 00000000..2cdf5c6a --- /dev/null +++ b/.github/workflows/checkStyle.yml @@ -0,0 +1,39 @@ +name: Check Style + +on: + pull_request: + +env: + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3g" + +jobs: + checkStyle: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Setup JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Detekt + run: ./gradlew detekt + + - name: Upload detekt SARIF files + if: always() + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'build/reports/detekt/merge.sarif' + category: detekt \ No newline at end of file diff --git a/.github/workflows/publishAndroid.yml b/.github/workflows/publishAndroid.yml index 12c6228e..e0b5e3be 100644 --- a/.github/workflows/publishAndroid.yml +++ b/.github/workflows/publishAndroid.yml @@ -1,4 +1,7 @@ name: Publish Android +concurrency: # Cancel currently running releases when a new one is started + group: publish-android + cancel-in-progress: true on: push: branches: @@ -19,10 +22,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v3 - - name: set up JDK 11 + - name: set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - name: Setup Gradle @@ -55,11 +58,10 @@ jobs: GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3g" - name: Create version tag - if: ${{ github.ref_name == 'main' }} run: | git tag android-$(cat androidApp/build/version.tag) - git push --tags + git push origin android-$(cat androidApp/build/version.tag) # TODO auto increase androidPatch in version.properties (on prod build only) and push (to dev branch, republish must be prevented then)? - # TODO add discord or other notification \ No newline at end of file + # TODO add discord or other notification diff --git a/.github/workflows/publishShared.yml b/.github/workflows/publishShared.yml index c208bd5e..5bb8d4f9 100644 --- a/.github/workflows/publishShared.yml +++ b/.github/workflows/publishShared.yml @@ -1,4 +1,7 @@ name: Publish Shared Swift Package +concurrency: # Cancel currently running releases when a new one is started + group: publish-shared + cancel-in-progress: true on: workflow_dispatch: # allow manually running the workflow for the dev and main branch branches: @@ -17,8 +20,8 @@ on: jobs: call-kmmbridge-publish: - uses: touchlab/KMMBridgeGithubWorkflow/.github/workflows/faktorybuildbranches.yml@v0.8 + uses: touchlab/KMMBridgeGithubWorkflow/.github/workflows/faktorybuildbranches.yml@2e121ace461e0004eb079926c1f6e74afaee3e3d with: - jvmVersion: 11 + jvmVersion: 17 secrets: gradle_params: "-PGITHUB_BRANCH=${{ github.ref_name }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1afc827b..38a9d88c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,10 +15,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v3 - - name: set up JDK 11 + - name: set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - name: Setup Gradle diff --git a/README.md b/README.md index 596440f0..dcf22eec 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ and [build.gradle.kts (shared)](shared/build.gradle.kts) kmmbridge config) # Further Resources and References - [Kotlin/kmm-sample](https://github.com/Kotlin/kmm-sample) -- [KaMPKit](https://github.com/touchlab/KaMPKit) Collection of code and tools for getting stated +- [KaMPKit](https://github.com/touchlab/KaMPKit) Collection of code and tools for getting started with KMP/KMM - [KMMBridge SPM sample (android + shared)](https://github.com/touchlab/KMMBridgeSampleKotlin) - [KMMBridge SPM sample (iOS)](https://github.com/touchlab/KMMBridgeSampleSpm) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 815cd5e2..7ddcdc73 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -6,9 +6,9 @@ import java.util.Properties plugins { id("com.android.application") - id("com.github.triplet.play") version "3.6.0" + id("com.github.triplet.play") version "3.8.4" kotlin("android") - id("com.google.devtools.ksp") version "1.8.10-1.0.9" + id("com.google.devtools.ksp") version "1.9.0-1.0.13" } private val versionProperty by lazy { @@ -23,6 +23,10 @@ android { namespace = "org.datepollsystems.waiterrobot.android" compileSdk = Versions.androidCompileSdk + androidResources { + generateLocaleConfig = true + } + defaultConfig { applicationId = "org.datepollsystems.waiterrobot.android" @@ -73,21 +77,22 @@ android { buildFeatures { compose = true + buildConfig = true } composeOptions { kotlinCompilerExtensionVersion = Versions.composeCompiler } - packagingOptions { + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } @@ -97,7 +102,6 @@ android { create("lava") { dimension = "environment" applicationIdSuffix = ".lava" - resValue("string", "app_name", "WaiterRobot Lava") buildConfigField("String", "API_BASE", "\"https://lava.kellner.team/api\"") manifestPlaceholders["host"] = "lava.kellner.team" @@ -107,13 +111,12 @@ android { // -> use epochMinutes (overflow would be in 5962). // (conversion to int is save as java int is bigger as the max versionCode allowed by google play) val epochMinutes = (Date().toInstant().epochSecond / 60).toInt() - versionNameSuffix = "-lava-${epochMinutes}" + versionNameSuffix = "-lava-$epochMinutes" versionCode = epochMinutes } create("prod") { dimension = "environment" - resValue("string", "app_name", "WaiterRobot") buildConfigField("String", "API_BASE", "\"https://my.kellner.team/api\"") manifestPlaceholders["host"] = "my.kellner.team" } @@ -158,11 +161,11 @@ dependencies { implementation("androidx.lifecycle:lifecycle-runtime-ktx:${Versions.androidxLifecycle}") implementation("androidx.appcompat:appcompat:1.6.1") - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.2") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") // Compose runtimeOnly("androidx.compose.compiler:compiler:${Versions.composeCompiler}") - implementation("androidx.activity:activity-compose:1.6.1") + implementation("androidx.activity:activity-compose:1.7.2") implementation("androidx.compose.foundation:foundation:${Versions.compose}") implementation("androidx.compose.foundation:foundation-layout:${Versions.compose}") implementation("androidx.compose.ui:ui-graphics:${Versions.compose}") @@ -174,13 +177,13 @@ dependencies { implementation("androidx.compose.material:material-icons-extended:${Versions.compose}") // Compose helpers - implementation("com.google.accompanist:accompanist-permissions:0.28.0") + implementation("com.google.accompanist:accompanist-permissions:0.32.0") // Architecture (MVI) implementation("org.orbit-mvi:orbit-compose:${Versions.orbitMvi}") // Dependency injection - implementation("io.insert-koin:koin-androidx-compose:3.4.2") // Not aligned with other koin version + implementation("io.insert-koin:koin-androidx-compose:3.4.6") // Not aligned with other koin version // SafeCompose Navigation Args implementation("io.github.raamcosta.compose-destinations:core:${Versions.composeDestinations}") @@ -192,7 +195,7 @@ dependencies { implementation("androidx.camera:camera-lifecycle:${Versions.camera}") // QrCode Scanning - implementation("com.google.mlkit:barcode-scanning:17.0.3") + implementation("com.google.mlkit:barcode-scanning:17.2.0") // In-App-Update support implementation("com.google.android.play:app-update:2.1.0") diff --git a/androidApp/src/lava/res/values-de/strings.xml b/androidApp/src/lava/res/values-de/strings.xml new file mode 100644 index 00000000..e8eb84b1 --- /dev/null +++ b/androidApp/src/lava/res/values-de/strings.xml @@ -0,0 +1,4 @@ + + + lava kellner.team + \ No newline at end of file diff --git a/androidApp/src/lava/res/values/strings.xml b/androidApp/src/lava/res/values/strings.xml new file mode 100644 index 00000000..b4d129a8 --- /dev/null +++ b/androidApp/src/lava/res/values/strings.xml @@ -0,0 +1,4 @@ + + + lava waiters.team + diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/MainActivity.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/MainActivity.kt index c973eb15..30623f66 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/MainActivity.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/MainActivity.kt @@ -19,6 +19,7 @@ import kotlinx.datetime.Instant import kotlinx.datetime.until import org.datepollsystems.waiterrobot.android.ui.RootView import org.datepollsystems.waiterrobot.shared.core.CommonApp +import org.datepollsystems.waiterrobot.shared.core.CommonApp.MIN_UPDATE_INFO_HOURS import org.datepollsystems.waiterrobot.shared.features.settings.models.AppTheme import org.datepollsystems.waiterrobot.shared.root.RootViewModel import org.datepollsystems.waiterrobot.shared.utils.extensions.defaultOnNull @@ -74,11 +75,11 @@ class MainActivity : AppCompatActivity() { val appUpdateInfoTask = appUpdateManager.appUpdateInfo appUpdateInfoTask.addOnSuccessListener { appUpdateInfo -> if ( - appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE - && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) - && CommonApp.settings.lastUpdateAvailableNote // Show max once a day + appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && + appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE) && + CommonApp.settings.lastUpdateAvailableNote // Show max once a day .defaultOnNull(Instant.DISTANT_PAST) - .until(Clock.System.now(), DateTimeUnit.HOUR) > 24 + .until(Clock.System.now(), DateTimeUnit.HOUR) > MIN_UPDATE_INFO_HOURS ) { CommonApp.settings.lastUpdateAvailableNote = Clock.System.now() appUpdateManager.startUpdateFlow( diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/billing/BillingScreen.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/billing/BillingScreen.kt index 94aabf9c..74d8d170 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/billing/BillingScreen.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/billing/BillingScreen.kt @@ -28,8 +28,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.navigation.NavController import com.ramcosta.composedestinations.annotation.Destination +import org.datepollsystems.waiterrobot.android.ui.common.CenteredText import org.datepollsystems.waiterrobot.android.ui.common.FloatingActionButton -import org.datepollsystems.waiterrobot.android.ui.core.CenteredText import org.datepollsystems.waiterrobot.android.ui.core.handleSideEffects import org.datepollsystems.waiterrobot.android.ui.core.view.ScaffoldView import org.datepollsystems.waiterrobot.shared.core.viewmodel.ViewState diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/billing/PayBillDialog.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/billing/PayBillDialog.kt index 3f9b1fed..bf54e204 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/billing/PayBillDialog.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/billing/PayBillDialog.kt @@ -1,8 +1,16 @@ package org.datepollsystems.waiterrobot.android.ui.billing -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Surface +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -15,8 +23,8 @@ import org.datepollsystems.waiterrobot.shared.generated.localization.change import org.datepollsystems.waiterrobot.shared.generated.localization.pay import org.datepollsystems.waiterrobot.shared.generated.localization.total -@Composable // TODO own Screen? +@Composable fun PayBillDialog( priceSum: String, changeText: String, diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/CenteredText.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/CenteredText.kt index e7d96eca..90adaa80 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/CenteredText.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/CenteredText.kt @@ -1,4 +1,4 @@ -package org.datepollsystems.waiterrobot.android.ui.core +package org.datepollsystems.waiterrobot.android.ui.common import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/FloatingActionButton.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/FloatingActionButton.kt index b58710ee..8227a51e 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/FloatingActionButton.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/FloatingActionButton.kt @@ -2,7 +2,11 @@ package org.datepollsystems.waiterrobot.android.ui.common import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material.* +import androidx.compose.material.ContentAlpha +import androidx.compose.material.FloatingActionButtonDefaults +import androidx.compose.material.FloatingActionButtonElevation +import androidx.compose.material.MaterialTheme +import androidx.compose.material.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -10,8 +14,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver -@Composable /* Wrapper for FloatingActionButton which allows to disable the button */ +@Composable fun FloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, @@ -23,7 +27,9 @@ fun FloatingActionButton( elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), content: @Composable () -> Unit ) { - val realOnclick = if (enabled) onClick else { + val realOnclick = if (enabled) { + onClick + } else { {} } val realBackgroundColor = if (enabled) { diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/LinkText.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/LinkText.kt index cdbcd185..0266fb9d 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/LinkText.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/LinkText.kt @@ -43,4 +43,4 @@ fun LinkText(modifier: Modifier = Modifier, text: String, url: String) { } } ) -} \ No newline at end of file +} diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/SingleSelectDialog.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/SingleSelectDialog.kt index 1029e33c..d218b68f 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/SingleSelectDialog.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/SingleSelectDialog.kt @@ -1,6 +1,11 @@ package org.datepollsystems.waiterrobot.android.ui.common -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectable @@ -77,4 +82,4 @@ private fun RadioButtonRow( modifier = Modifier.padding(start = 16.dp) ) } -} \ No newline at end of file +} diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/SwipeableListItem.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/SwipeableListItem.kt index 7a777bfb..db6bf9fa 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/SwipeableListItem.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/common/SwipeableListItem.kt @@ -6,11 +6,24 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.DismissDirection +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FractionalThreshold +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.SwipeToDismiss import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -120,4 +133,4 @@ fun SwipeableListItem( content() } } -} \ No newline at end of file +} diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/Navigation.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/Navigation.kt index 872030e4..371a8621 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/Navigation.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/Navigation.kt @@ -16,7 +16,7 @@ import org.datepollsystems.waiterrobot.shared.core.navigation.Screen import org.datepollsystems.waiterrobot.shared.core.viewmodel.AbstractViewModel import org.datepollsystems.waiterrobot.shared.core.viewmodel.ViewModelEffect import org.datepollsystems.waiterrobot.shared.core.viewmodel.ViewModelState -import org.koin.androidx.compose.get +import org.koin.compose.koinInject import org.koin.core.parameter.parametersOf import org.orbitmvi.orbit.compose.collectSideEffect @@ -26,7 +26,7 @@ fun AbstractViewModel.handleSide navigator: NavController, handler: (suspend (E) -> Unit)? = null ) { - val logger: Logger = get { parametersOf("handleSideEffects") } + val logger: Logger = koinInject { parametersOf("handleSideEffects") } collectSideEffect { navOrSideEffect -> when (navOrSideEffect) { is NavOrViewModelEffect.NavEffect -> navigator.handleNavAction(navOrSideEffect.action) diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/theme/WaiterRobotTheme.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/theme/WaiterRobotTheme.kt index 1c75f718..6082229e 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/theme/WaiterRobotTheme.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/theme/WaiterRobotTheme.kt @@ -15,9 +15,11 @@ fun WaiterRobotTheme( useDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { - Box(modifier = Modifier - .systemBarsPadding() - .imePadding()) { + Box( + modifier = Modifier + .systemBarsPadding() + .imePadding() + ) { MaterialTheme( colors = if (useDarkTheme) darkColors else lightColors, content = content diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/view/View.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/view/View.kt index b6fb0b77..06172f32 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/view/View.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/core/view/View.kt @@ -15,7 +15,8 @@ import org.datepollsystems.waiterrobot.shared.core.viewmodel.ViewState /** * Handles displaying errors and loading state. - * If [onRefresh] is provided a [RefreshableView] is used and [content] must therefore be scrollable ([RefreshableView]). Otherwise a [LoadableView]. + * If [onRefresh] is provided a [RefreshableView] is used and [content] therefore + * must be scrollable ([RefreshableView]). Otherwise a [LoadableView] is used. * @see ErrorDialog * @see RefreshableView * @see LoadableView @@ -59,7 +60,6 @@ fun View( content: @Composable () -> Unit ) = View(state, Modifier.padding(paddingValues), onRefresh, content) - @Composable fun ScaffoldView( state: ViewModelState, diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/AddNoteDialog.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/AddNoteDialog.kt index d9169820..863ec849 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/AddNoteDialog.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/AddNoteDialog.kt @@ -1,14 +1,36 @@ package org.datepollsystems.waiterrobot.android.ui.order -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import org.datepollsystems.waiterrobot.shared.features.order.models.OrderItem -import org.datepollsystems.waiterrobot.shared.generated.localization.* +import org.datepollsystems.waiterrobot.shared.generated.localization.L +import org.datepollsystems.waiterrobot.shared.generated.localization.cancel +import org.datepollsystems.waiterrobot.shared.generated.localization.clear +import org.datepollsystems.waiterrobot.shared.generated.localization.inputLabel +import org.datepollsystems.waiterrobot.shared.generated.localization.inputPlaceholder +import org.datepollsystems.waiterrobot.shared.generated.localization.save +import org.datepollsystems.waiterrobot.shared.generated.localization.title @Composable fun AddNoteDialog(item: OrderItem, onDismiss: () -> Unit, onSave: (note: String?) -> Unit) { @@ -33,7 +55,9 @@ fun AddNoteDialog(item: OrderItem, onDismiss: () -> Unit, onSave: (note: String? label = { Text(text = L.order.addNoteDialog.inputLabel()) }, placeholder = { Text(text = L.order.addNoteDialog.inputPlaceholder()) }, value = note, - onValueChange = { note = it.take(120) } + onValueChange = { note = it.take(120) }, + minLines = 3, + maxLines = 3 ) Text( modifier = Modifier.fillMaxWidth(), diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/OrderListItem.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/OrderListItem.kt index ad0f9d90..f005d248 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/OrderListItem.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/OrderListItem.kt @@ -62,6 +62,7 @@ private fun OrderListItemPreview() { amount = 10, note = "test Note", addAction = { _, _ -> }, - onLongClick = {}) + onLongClick = {} + ) } } diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/OrderScreen.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/OrderScreen.kt index 1b9dae7e..8e62f8ad 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/OrderScreen.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/OrderScreen.kt @@ -18,8 +18,8 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.ramcosta.composedestinations.annotation.Destination import kotlinx.coroutines.launch +import org.datepollsystems.waiterrobot.android.ui.common.CenteredText import org.datepollsystems.waiterrobot.android.ui.common.FloatingActionButton -import org.datepollsystems.waiterrobot.android.ui.core.CenteredText import org.datepollsystems.waiterrobot.android.ui.core.handleSideEffects import org.datepollsystems.waiterrobot.android.ui.core.view.ScaffoldView import org.datepollsystems.waiterrobot.shared.core.viewmodel.ViewState @@ -49,7 +49,8 @@ fun OrderScreen( var noteDialogItem: OrderItem? by remember { mutableStateOf(null) } val bottomSheetState = rememberModalBottomSheetState( - // When opening the order screen waiter most likely wants to add a new product -> show the product list immediately + // When opening the order screen waiter most likely wants to add a new product + // -> show the product list immediately // But don't show it when the screen was opened with an initial item, this feels not nice initialValue = if (initialItemId == null) ModalBottomSheetValue.Expanded else ModalBottomSheetValue.Hidden, skipHalfExpanded = true diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/Product.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/Product.kt index ffff64b5..44fca31e 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/Product.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/Product.kt @@ -40,13 +40,14 @@ fun Product( textAlign = TextAlign.Center, textDecoration = if (product.soldOut) TextDecoration.LineThrough else null ) - Text( - text = product.allergens.joinToString(", ") { it.shortName } - .ifEmpty { "-" }, - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - color = Color.LightGray - ) + if (product.allergens.isNotEmpty()) { + Text( + text = product.allergens.joinToString(", ") { it.shortName }, + style = MaterialTheme.typography.caption, + textAlign = TextAlign.Center, + color = Color.LightGray + ) + } Text( text = product.price.toString(), style = MaterialTheme.typography.body2, diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/ProductSearch.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/ProductSearch.kt index 7f9ae4b2..ea8cbbc6 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/ProductSearch.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/order/ProductSearch.kt @@ -21,11 +21,10 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch +import org.datepollsystems.waiterrobot.android.ui.common.CenteredText import org.datepollsystems.waiterrobot.android.ui.common.sectionHeader -import org.datepollsystems.waiterrobot.android.ui.core.CenteredText import org.datepollsystems.waiterrobot.shared.features.order.models.Product import org.datepollsystems.waiterrobot.shared.features.order.models.ProductGroup -import org.datepollsystems.waiterrobot.shared.features.order.models.ProductGroupWithProducts import org.datepollsystems.waiterrobot.shared.generated.localization.L import org.datepollsystems.waiterrobot.shared.generated.localization.allGroups import org.datepollsystems.waiterrobot.shared.generated.localization.noProductFound @@ -35,7 +34,7 @@ import org.datepollsystems.waiterrobot.shared.generated.localization.title @OptIn(ExperimentalFoundationApi::class) @Composable fun ProductSearch( - productGroups: List, + productGroups: List, onSelect: (Product) -> Unit, onFilter: (String) -> Unit, close: () -> Unit @@ -74,7 +73,8 @@ fun ProductSearch( }, singleLine = true, keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Text, imeAction = ImeAction.Search + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search ), modifier = Modifier .padding(start = 10.dp, end = 20.dp, top = 10.dp, bottom = 10.dp) @@ -95,14 +95,17 @@ fun ProductSearch( edgePadding = 0.dp, divider = {} // Add divider externally as otherwise it does not span the whole width ) { - Tab(selected = pagerState.currentPage == 0, + Tab( + selected = pagerState.currentPage == 0, onClick = { coScope.launch { pagerState.scrollToPage(0) } }, - text = { Text(L.productSearch.allGroups()) }) - productGroups.forEachIndexed { index, productGroupWithProducts -> + text = { Text(L.productSearch.allGroups()) } + ) + productGroups.forEachIndexed { index, productGroup -> Tab( selected = pagerState.currentPage == index + 1, onClick = { coScope.launch { pagerState.scrollToPage(index + 1) } }, - text = { Text(productGroupWithProducts.group.name) }) + text = { Text(productGroup.name) } + ) } } @@ -111,10 +114,13 @@ fun ProductSearch( HorizontalPager(pagerState) { pageIndex -> if (pageIndex == 0) { ProductLazyVerticalGrid { - productGroups.forEach { (group: ProductGroup, products: List) -> - if (products.isNotEmpty()) { - sectionHeader(key = "group-${group.id}", title = group.name) - items(products, key = Product::id) { product -> + productGroups.forEach { productGroup -> + if (productGroup.products.isNotEmpty()) { + sectionHeader( + key = "group-${productGroup.id}", + title = productGroup.name + ) + items(productGroup.products, key = Product::id) { product -> Product(product = product, onSelect = { onSelect(product) }) } } diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/scanner/QrCodeScanner.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/scanner/QrCodeScanner.kt index 7fe7d9f3..5385048e 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/scanner/QrCodeScanner.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/scanner/QrCodeScanner.kt @@ -82,30 +82,36 @@ fun QrCodeScanner(onResult: (Barcode) -> Unit) { val analysisUseCase = ImageAnalysis.Builder() .setTargetResolution(Size(previewView.width, previewView.height)) - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) // Do not process every frame only keep the latest + // Do not process every frame only keep the latest + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .apply { setAnalyzer( - Executors.newSingleThreadExecutor(), // Execute analysis on a single worker thread + // Execute analysis on a single worker thread + Executors.newSingleThreadExecutor(), QrCodeAnalyzer { - this.clearAnalyzer() // Stop after first detected code // TODO test scanning invalid code (no data and no url) + // Stop after first detected code + // TODO test scanning invalid code (no data and no url) + this.clearAnalyzer() onResult(it.first()) } ) } coroutineScope.launch { + @Suppress("TooGenericExceptionCaught") try { val cameraProvider = context.getCameraProvider() - // Make sure no use case is bound to the cameraProvider, when QrCodeScanner is "launched" twice - // (e.g. when first scan at login fails and the QrCodeScanner is then opened a second time) - if (!cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA)) { errorMessage = L.qrScanner.noCameraFound() return@launch } + // Make sure no use case is bound to the cameraProvider, + // when QrCodeScanner is "launched" twice (e.g. when first scan at + // login fails and the QrCodeScanner is then opened a second time) cameraProvider.unbindAll() + cam = cameraProvider.bindToLifecycle( lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/settings/SettingsScreen.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/settings/SettingsScreen.kt index f1acc66f..eafac06c 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/settings/SettingsScreen.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/settings/SettingsScreen.kt @@ -124,7 +124,11 @@ fun SettingsScreen( settingsItem( icon = { Icon(Icons.Filled.Logout, contentDescription = "Logout") }, title = { Text(L.settings.logout.action()) }, - subtitle = { Text("\"${CommonApp.settings.organisationName}\" / \"${CommonApp.settings.waiterName}\"") }, + subtitle = { + Text( + "\"${CommonApp.settings.organisationName}\" / \"${CommonApp.settings.waiterName}\"" + ) + }, onClick = { showLogoutWarningDialog = true } ) settingsItem( diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/switchevent/SwitchEventScreen.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/switchevent/SwitchEventScreen.kt index 818caf4e..c49fe0a2 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/switchevent/SwitchEventScreen.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/switchevent/SwitchEventScreen.kt @@ -48,7 +48,8 @@ fun SwitchEventScreen( Scaffold(scaffoldState = LocalScaffoldState.current) { Column(modifier = Modifier.padding(it)) { - // Surface wrapper container is needed as otherwise the PullRefreshIndicator would be on top of this part of the view + // Surface wrapper container is needed as otherwise the PullRefreshIndicator would be + // on top of this part of the view Surface(modifier = Modifier.zIndex(1f)) { Column { Icon( diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/tabledetail/TableDetailScreen.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/tabledetail/TableDetailScreen.kt index 45521381..4342f10f 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/tabledetail/TableDetailScreen.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/tabledetail/TableDetailScreen.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.ramcosta.composedestinations.annotation.Destination -import org.datepollsystems.waiterrobot.android.ui.core.CenteredText +import org.datepollsystems.waiterrobot.android.ui.common.CenteredText import org.datepollsystems.waiterrobot.android.ui.core.handleSideEffects import org.datepollsystems.waiterrobot.android.ui.core.view.ScaffoldView import org.datepollsystems.waiterrobot.shared.features.table.models.OrderedItem diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/tablelist/TableListScreen.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/tablelist/TableListScreen.kt index 697cb6b7..670c88c4 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/tablelist/TableListScreen.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/ui/tablelist/TableListScreen.kt @@ -15,8 +15,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import org.datepollsystems.waiterrobot.android.ui.common.CenteredText import org.datepollsystems.waiterrobot.android.ui.common.sectionHeader -import org.datepollsystems.waiterrobot.android.ui.core.CenteredText import org.datepollsystems.waiterrobot.android.ui.core.handleSideEffects import org.datepollsystems.waiterrobot.android.ui.core.view.ScaffoldView import org.datepollsystems.waiterrobot.shared.core.CommonApp diff --git a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/util/QrCodeAnalyzer.kt b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/util/QrCodeAnalyzer.kt index 4dd7b1d6..43e64ebe 100644 --- a/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/util/QrCodeAnalyzer.kt +++ b/androidApp/src/main/java/org/datepollsystems/waiterrobot/android/util/QrCodeAnalyzer.kt @@ -10,13 +10,14 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage +import org.datepollsystems.waiterrobot.shared.core.di.injectLoggerForClass import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import java.util.concurrent.TimeUnit -class QrCodeAnalyzer(private val onQrCode: (List) -> Unit) : ImageAnalysis.Analyzer, - KoinComponent { - private val logger: Logger by inject() +class QrCodeAnalyzer( + private val onQrCode: (List) -> Unit +) : ImageAnalysis.Analyzer, KoinComponent { + private val logger: Logger by injectLoggerForClass() private var lastAnalyzedTimeStamp = 0L diff --git a/androidApp/src/main/res/resources.properties b/androidApp/src/main/res/resources.properties new file mode 100644 index 00000000..fccdea81 --- /dev/null +++ b/androidApp/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=de \ No newline at end of file diff --git a/androidApp/src/main/res/values-de/strings.xml b/androidApp/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..b838b33d --- /dev/null +++ b/androidApp/src/main/res/values-de/strings.xml @@ -0,0 +1,4 @@ + + + kellner.team + \ No newline at end of file diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml new file mode 100644 index 00000000..a9361b73 --- /dev/null +++ b/androidApp/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + waiters.team + diff --git a/androidApp/version.properties b/androidApp/version.properties index 98b424fc..59dc3add 100644 --- a/androidApp/version.properties +++ b/androidApp/version.properties @@ -1 +1 @@ -androidVersion=2.0.2 \ No newline at end of file +androidVersion=2.0.3 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8c8e3ad9..700a9b09 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,6 @@ +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.report.ReportMergeTask + buildscript { repositories { gradlePluginPortal() @@ -5,17 +8,50 @@ buildscript { mavenCentral() } dependencies { - val kotlinVersion = "1.8.10" - classpath("com.android.tools.build:gradle:7.4.1") + val kotlinVersion = "1.9.0" + classpath("com.android.tools.build:gradle:8.1.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}") classpath("org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}") } } +plugins { + id("io.gitlab.arturbosch.detekt") version "1.23.1" +} + +val detektReportMergeSarif by tasks.registering(ReportMergeTask::class) { + output = layout.buildDirectory.file("reports/detekt/merge.sarif") +} + allprojects { repositories { gradlePluginPortal() google() mavenCentral() } + + apply(plugin = "io.gitlab.arturbosch.detekt") + + detekt { + config.from(rootDir.resolve("detekt.yml")) + buildUponDefaultConfig = true + basePath = rootDir.path + // Autocorrection can only be done locally + autoCorrect = System.getenv("CI")?.lowercase() != true.toString() + } + + dependencies { + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.1") + } + + tasks.withType().configureEach { + reports { + html.required = true + sarif.required = true + } + finalizedBy(detektReportMergeSarif) + } + detektReportMergeSarif { + input.from(tasks.withType().map { it.sarifReportFile }) + } } diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 3e9366d5..034e5da0 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -1,22 +1,22 @@ object Versions { // Shared const val kermitLogger = "1.2.2" - const val koinDi = "3.3.3" + const val koinDi = "3.4.3" const val orbitMvi = "4.5.0" - const val mokoMvvm = "0.15.0" - const val ktor = "2.2.3" + const val mokoMvvm = "0.16.1" + const val ktor = "2.3.3" const val settings = "1.0.0" - const val realm = "1.6.1" // Also update belonging plugin in shared/build.gradle.kts + const val realm = "1.10.2" // Also update belonging plugin in shared/build.gradle.kts // Android const val androidMinSdk = 24 - const val androidTargetSdk = 31 - const val androidCompileSdk = 33 - const val androidBuildTools = "33.0.0" + const val androidTargetSdk = 33 + const val androidCompileSdk = 34 + const val androidBuildTools = "34.0.0" - const val compose = "1.5.0-beta01" - const val composeCompiler = "1.4.4" - const val androidxLifecycle = "2.5.1" - const val composeDestinations = "1.9.42-beta" - const val camera = "1.2.1" + const val compose = "1.5.0" + const val composeCompiler = "1.5.1" + const val androidxLifecycle = "2.6.1" + const val composeDestinations = "1.9.52" + const val camera = "1.2.3" } diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 00000000..bb4bf5f8 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,38 @@ +naming: + FunctionNaming: + ignoreAnnotated: + - Composable + +complexity: + LongMethod: + ignoreAnnotated: + - Composable + LongParameterList: + ignoreDefaultParameters: true + ignoreAnnotated: + - Composable + CyclomaticComplexMethod: + ignoreSimpleWhenEntries: true + +style: + UnusedPrivateMember: + ignoreAnnotated: + - Preview + WildcardImport: + active: false + MagicNumber: + ignoreAnnotated: + - Composable + +# Formatting contains some overlapping rules with the standard rule set -> deactivate them so we do not get duplicate errors +formatting: + NoWildcardImports: # style>WildcardImport + active: false + Filename: # naming>MatchingDeclarationName + active: false + FinalNewline: # style>NewLineAtEndOfFile + active: false + MaximumLineLength: # style>MaxLineLength + active: false + ModifierOrdering: # style>ModifierOrder + active: false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c0..7f93135c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 376271f8..ac72c34e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Tue Nov 22 09:55:58 CET 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..0adc8e1a 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,99 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32..6689b85b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,92 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 00000000..64811a27 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,36 @@ +{ + "baseBranches": [ + "develop" + ], + "semanticPrefix": "renovate", + "labels": [ + "dependencies" + ], + "schedule": [ + "after 10pm every weekday", + "before 4am every weekday", + "every weekend" + ], + "pinVersions": false, + "semanticCommits": "enabled", + "separateMajorMinor": false, + "prHourlyLimit": 2, + "timezone": "Europe/Vienna", + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "patch", + "pin", + "digest" + ], + "groupName": "minor-updates" + }, + { + "matchUpdateTypes": [ + "major" + ], + "groupName": "major-risky-update" + } + ] +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8d40f2b2..e51bf176 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,4 +15,34 @@ dependencyResolutionManagement { rootProject.name = "WaiterRobot" include(":androidApp") -include(":shared") \ No newline at end of file +include(":shared") + +plugins { + id("org.danilopianini.gradle-pre-commit-git-hooks") version "1.1.9" +} + +gitHooks { + hook("pre-push") { + from { + """ + echo "Running detekt check..." + OUTPUT="/tmp/detekt-${'$'}(date +%s)" + ./gradlew detekt > ${'$'}OUTPUT + EXIT_CODE=${'$'}? + if [ ${'$'}EXIT_CODE -ne 0 ]; then + cat ${'$'}OUTPUT + rm ${'$'}OUTPUT + echo "**********************************************************************************************" + echo " detekt failed " + echo " Please fix the above issues before pushing. " + echo " Some of the issues might already be resolved automatically and only must be committed again. " + echo " Run './gradlew detekt' to to get an updated list of issues. " + echo "**********************************************************************************************" + exit ${'$'}EXIT_CODE + fi + rm ${'$'}OUTPUT + """.trimIndent() + } + } + createHooks(overwriteExisting = true) +} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index d15fac2b..2723a2e1 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -14,8 +14,8 @@ plugins { id("com.android.library") id("co.touchlab.faktory.kmmbridge") version "0.3.7" `maven-publish` - id("dev.jamiecraane.plugins.kmmresources") version "1.0.0-alpha10" // Shared localization - id("io.realm.kotlin") version "1.6.1" + id("dev.jamiecraane.plugins.kmmresources") version "1.0.0-alpha11" // Shared localization + id("io.realm.kotlin") version "1.10.2" } version = "1.0" // Shared package has only 2 digit version, patch is managed by kmmbridge. @@ -69,7 +69,7 @@ kotlin { // Helper api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") - api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") // Also needed by android for ComposeDestination parameter serialization + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") // Also needed by android for ComposeDestination parameter serialization } } val commonTest by getting { @@ -90,7 +90,7 @@ kotlin { implementation("io.ktor:ktor-client-cio:${Versions.ktor}") } } - val androidTest by getting + val androidUnitTest by getting val iosX64Main by getting val iosArm64Main by getting @@ -194,7 +194,7 @@ tasks { copy { from("$generatedLocalizationRoot/commonMain/resources/ios") into( - "${project.buildDir}/XCFrameworks/${buildType.toLowerCase()}/" + + "${project.buildDir}/XCFrameworks/${buildType.lowercase()}/" + "$iosFrameworkName.xcframework/$arch/$iosFrameworkName.framework" ) } @@ -233,3 +233,11 @@ class CustomGitVersionManager( } } } + +detekt { + source.from( + "src/androidMain/kotlin", + "src/commonMain/kotlin", + "src/iosMain/kotlin", + ) +} diff --git a/shared/localization.yml b/shared/localization.yml index 03745ad3..d827f488 100644 --- a/shared/localization.yml +++ b/shared/localization.yml @@ -1,7 +1,7 @@ app: name: - en: WaiterRobot - de: WaiterRobot + en: waiters.team + de: kellner.team genericError: message: en: Something went wrong. Please try again. @@ -233,7 +233,7 @@ settings: darkMode: title: en: Dark Mode - de: Dunklermodus + de: Dunkelmodus useSystem: en: Use system setting de: System einstellung verwenden diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/AppInfo.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/AppInfo.kt index 15ad183c..c1431bd8 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/AppInfo.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/AppInfo.kt @@ -11,8 +11,7 @@ class AppInfo( ) { val apiBaseUrl = apiBaseUrl.removeSuffix("/") + "/" - val sessionName = - "$os; $appVersion ($appBuild); $phoneModel".truncate(60) + val sessionName = "$os; $appVersion ($appBuild); $phoneModel".truncate(MAX_SESSION_LENGTH) } sealed class OS { @@ -26,3 +25,5 @@ sealed class OS { is Ios -> "iOS-${this.version}" } } + +private const val MAX_SESSION_LENGTH = 60 diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/CommonApp.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/CommonApp.kt index 0a796637..67a6874f 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/CommonApp.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/CommonApp.kt @@ -54,6 +54,7 @@ object CommonApp : KoinComponent { internal fun logout() { coroutineScope.launch { + @Suppress("TooGenericExceptionCaught") try { val tokens = settings.tokens ?: return@launch getKoin().getOrNull()?.logout(tokens) @@ -77,4 +78,6 @@ object CommonApp : KoinComponent { ?.forEach(BearerAuthProvider::clearToken) } } + + const val MIN_UPDATE_INFO_HOURS = 24 } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/AbstractApi.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/AbstractApi.kt index fc3b406d..7784c946 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/AbstractApi.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/AbstractApi.kt @@ -39,10 +39,10 @@ internal abstract class AbstractApi(basePath: String, private val client: HttpCl block?.invoke(this) } - protected suspend fun post( + protected suspend inline fun post( endpoint: String = "", - body: RequestBodyDto? = null, - block: (HttpRequestBuilder.() -> Unit)? = null + body: B? = null, + noinline block: (HttpRequestBuilder.() -> Unit)? = null ): HttpResponse = client.post(endpoint.toFullUrl()) { if (body != null) { contentType(ContentType.Application.Json) diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/ApiExceptions.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/ApiException.kt similarity index 100% rename from shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/ApiExceptions.kt rename to shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/ApiException.kt diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/AuthorizedClient.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/AuthorizedClient.kt index 5028295d..048f0926 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/AuthorizedClient.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/AuthorizedClient.kt @@ -33,6 +33,7 @@ internal fun createAuthorizedClient( // Function to refresh a token (called when server response with 401 and a WWW-Authenticate header) refreshTokens { + @Suppress("TooGenericExceptionCaught") try { authRepository.refreshTokens(scope).toBearerTokens() } catch (e: Exception) { diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/BasicClient.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/BasicClient.kt index ce4f5540..fd92f144 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/BasicClient.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/api/BasicClient.kt @@ -31,6 +31,7 @@ internal fun HttpClientConfig<*>.commonConfig( } install(HttpTimeout) { + @Suppress("MagicNumber") requestTimeoutMillis = 10_000 } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/db/AbstractDatabase.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/db/AbstractDatabase.kt index f309cad9..ad5179df 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/db/AbstractDatabase.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/db/AbstractDatabase.kt @@ -2,10 +2,11 @@ package org.datepollsystems.waiterrobot.shared.core.db import co.touchlab.kermit.Logger import io.realm.kotlin.Realm +import org.datepollsystems.waiterrobot.shared.core.di.injectLoggerForClass import org.koin.core.component.KoinComponent import org.koin.core.component.inject internal abstract class AbstractDatabase : KoinComponent { protected val realm: Realm by inject() - protected val logger: Logger by inject() + protected val logger: Logger by injectLoggerForClass() } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/db/DatabaseFactory.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/db/DatabaseFactory.kt index 8a37dc24..6832c4d1 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/db/DatabaseFactory.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/db/DatabaseFactory.kt @@ -3,21 +3,27 @@ package org.datepollsystems.waiterrobot.shared.core.db import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.types.RealmObject +import org.datepollsystems.waiterrobot.shared.features.order.db.model.AllergenEntry import org.datepollsystems.waiterrobot.shared.features.order.db.model.ProductEntry +import org.datepollsystems.waiterrobot.shared.features.order.db.model.ProductGroupEntry import org.datepollsystems.waiterrobot.shared.features.table.db.model.TableEntry import kotlin.reflect.KClass fun createRealmDB(): Realm { val schema: Set> = setOf( TableEntry::class, + ProductGroupEntry::class, ProductEntry::class, - ProductEntry.Allergen::class, - ProductEntry.ProductGroup::class, + AllergenEntry::class, ) + @Suppress("MagicNumber") val config = RealmConfiguration.Builder(schema) - .deleteRealmIfMigrationNeeded() // Realm is only used as a persistent cache - so do not care about migrations - .schemaVersion(2) // TODO increase with each version of the common code (automate - compute from version in buildScript or app version?) + // Realm is only used as a persistent cache - so do not care about migrations + .deleteRealmIfMigrationNeeded() + // TODO increase with each version of the common code + // (automate - compute from version in buildScript or app version?) + .schemaVersion(3) .build() return Realm.open(config) diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KtorLogger.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/CustomKtorLogger.kt similarity index 100% rename from shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KtorLogger.kt rename to shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/CustomKtorLogger.kt diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/Koin.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/Koin.kt index ffc2344f..cecb7ead 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/Koin.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/Koin.kt @@ -30,5 +30,5 @@ fun initKoin(appDeclaration: KoinAppDeclaration = { }) = startKoin { } fun KoinComponent.injectLogger(tag: String): Lazy = inject { parametersOf(tag) } -internal fun KoinComponent.injectLoggerForClass(): Lazy = +fun KoinComponent.injectLoggerForClass(): Lazy = injectLogger(this::class.simpleName ?: "AnonymousClass") diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KoinCoreModule.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KoinCoreModule.kt index 22b6604e..c62e5907 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KoinCoreModule.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KoinCoreModule.kt @@ -53,4 +53,7 @@ internal val coreModule = module { single { createRealmDB() } } -private fun createJson() = Json { ignoreUnknownKeys = true } +private fun createJson() = Json { + ignoreUnknownKeys = true + coerceInputValues = true // Use default value for null (when not-nullable) and unknown values +} diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/settings/SerializedStringSettings.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/settings/SerializedStringSettings.kt index 117025d0..8acb4e28 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/settings/SerializedStringSettings.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/settings/SerializedStringSettings.kt @@ -31,8 +31,9 @@ internal fun Settings.jsonSerialized( serializer: KSerializer ): ReadWriteProperty = JsonSerializedDelegate(this, key, defaultValue, serializer) -internal inline fun Settings.nullableJsonSerialized(key: String? = null) - : ReadWriteProperty = nullableJsonSerialized(key, serializer()) +internal inline fun Settings.nullableJsonSerialized( + key: String? = null +): ReadWriteProperty = nullableJsonSerialized(key, serializer()) internal fun Settings.nullableJsonSerialized( key: String? = null, diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/viewmodel/AbstractViewModel.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/viewmodel/AbstractViewModel.kt index b7c75872..1def6ded 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/viewmodel/AbstractViewModel.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/core/viewmodel/AbstractViewModel.kt @@ -46,7 +46,10 @@ abstract class AbstractViewModel(initia } else -> { - logger.w(exception) { "Unhandled exception in intent. Exceptions should be handled directly in the intent!" } + logger.w(exception) { + "Unhandled exception in intent. " + + "Exceptions should be handled directly in the intent!" + } intent { reduceError(L.app.genericError.title(), L.app.genericError.message()) } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/api/models/LogoutDto.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/api/models/LogoutRequestDto.kt similarity index 100% rename from shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/api/models/LogoutDto.kt rename to shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/api/models/LogoutRequestDto.kt diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/repository/AuthRepository.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/repository/AuthRepository.kt index b8811706..2be3cb74 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/repository/AuthRepository.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/repository/AuthRepository.kt @@ -55,6 +55,7 @@ internal class AuthRepository(private val authApi: AuthApi) : AbstractRepository } private suspend fun autoSelectEvent() { + @Suppress("TooGenericExceptionCaught") try { // Auto select event when there is only one available eventRepository.getEvents().singleOrNull()?.let { diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/viewmodel/register/RegisterViewModel.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/viewmodel/register/RegisterViewModel.kt index 15f0a59b..745919a5 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/viewmodel/register/RegisterViewModel.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/viewmodel/register/RegisterViewModel.kt @@ -21,7 +21,7 @@ class RegisterViewModel internal constructor( authRepository.createWithToken(createToken, name) navigator.popUpToRoot() reduce { state.withViewState(ViewState.Idle) } - } catch (e: ApiException.CredentialsIncorrect) { + } catch (_: ApiException.CredentialsIncorrect) { reduceError(L.login.invalidCode.title(), L.login.invalidCode.desc()) } } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/viewmodel/scanner/LoginScannerViewModel.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/viewmodel/scanner/LoginScannerViewModel.kt index 6da39d3d..535a4fc5 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/viewmodel/scanner/LoginScannerViewModel.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/auth/viewmodel/scanner/LoginScannerViewModel.kt @@ -25,7 +25,7 @@ class LoginScannerViewModel internal constructor( navigator.push(Screen.RegisterScreen(deepLink.token)) } } - } catch (e: Exception) { + } catch (_: Exception) { logger.d { "Error with scanned login code: $code" } reduceError("Invalid code", "Scanned invalid code") } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/billing/viewmodel/BillingState.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/billing/viewmodel/BillingState.kt index c8cd0807..222337f2 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/billing/viewmodel/BillingState.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/billing/viewmodel/BillingState.kt @@ -10,6 +10,7 @@ data class BillingState( val moneyGivenText: String = "", val changeText: String = "0.00 €", val showConfirmationDialog: Boolean = false, + @Suppress("ConstructorParameterNaming") internal val _billItems: Map = emptyMap() ) : ViewModelState() { val billItems: List by lazy { _billItems.values.toList() } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/billing/viewmodel/BillingViewModel.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/billing/viewmodel/BillingViewModel.kt index 40d016ce..2af6d8ee 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/billing/viewmodel/BillingViewModel.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/billing/viewmodel/BillingViewModel.kt @@ -43,7 +43,7 @@ class BillingViewModel internal constructor( try { val given = givenText.euro reduce { state.copy(changeText = (given - state.priceSum).toString()) } - } catch (e: Exception) { + } catch (_: Exception) { reduce { state.copy(changeText = "NaN") } } } @@ -86,7 +86,8 @@ class BillingViewModel internal constructor( } fun abortBill() = intent { - // Hide the confirmation dialog before navigation away, as otherwise on iOS it would be still shown on the new screen + // Hide the confirmation dialog before navigation away, + // as otherwise on iOS it would be still shown on the new screen reduce { state.copy(showConfirmationDialog = false) } navigator.pop() } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/OrderApi.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/OrderApi.kt index 3bc34f76..f8f40df1 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/OrderApi.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/OrderApi.kt @@ -9,4 +9,3 @@ internal class OrderApi(client: AuthorizedClient) : AuthorizedApi("waiter/order" post("/", order) } } - diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/ProductApi.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/ProductApi.kt index 122f1abc..4f1928c6 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/ProductApi.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/ProductApi.kt @@ -3,10 +3,10 @@ package org.datepollsystems.waiterrobot.shared.features.order.api import io.ktor.client.call.body import org.datepollsystems.waiterrobot.shared.core.api.AuthorizedApi import org.datepollsystems.waiterrobot.shared.core.api.AuthorizedClient -import org.datepollsystems.waiterrobot.shared.features.order.api.models.ProductGroupResponseDto +import org.datepollsystems.waiterrobot.shared.features.order.api.models.ProductGroupDto internal class ProductApi(client: AuthorizedClient) : AuthorizedApi("waiter/product", client) { suspend fun getProducts(eventId: Long) = - get("/", "eventId" to eventId.toString()).body>() + get("/", "eventId" to eventId.toString()).body>() } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/OrderDto.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/OrderRequestDto.kt similarity index 100% rename from shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/OrderDto.kt rename to shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/OrderRequestDto.kt diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/ProductDto.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/ProductDto.kt index 3bb2353b..8a36ea88 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/ProductDto.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/api/models/ProductDto.kt @@ -4,10 +4,11 @@ import kotlinx.serialization.Serializable import org.datepollsystems.waiterrobot.shared.utils.Cents @Serializable -internal class ProductGroupResponseDto( +internal class ProductGroupDto( val id: Long, val name: String, - val products: List + val products: List, + val position: Int = Int.MAX_VALUE, ) @Serializable @@ -16,7 +17,8 @@ internal class ProductDto( val name: String, val soldOut: Boolean, val price: Cents, - val allergens: List + val allergens: List, + val position: Int = Int.MAX_VALUE, ) @Serializable diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/db/ProductDatabase.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/db/ProductDatabase.kt index 5dd07c70..a3d641e3 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/db/ProductDatabase.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/db/ProductDatabase.kt @@ -5,33 +5,34 @@ import io.realm.kotlin.ext.query import io.realm.kotlin.query.RealmResults import org.datepollsystems.waiterrobot.shared.core.db.AbstractDatabase import org.datepollsystems.waiterrobot.shared.features.order.db.model.ProductEntry -import org.datepollsystems.waiterrobot.shared.features.table.db.model.TableEntry +import org.datepollsystems.waiterrobot.shared.features.order.db.model.ProductGroupEntry import org.datepollsystems.waiterrobot.shared.utils.extensions.Now import kotlin.time.Duration internal class ProductDatabase : AbstractDatabase() { - fun getForEvent(eventId: Long): RealmResults = - realm.query("eventId == $0", eventId).find() + fun getForEvent(eventId: Long): RealmResults = + realm.query("eventId == $0", eventId).find() - fun getById(id: Long): ProductEntry? = realm.query("id == $0", id).first().find() + fun getProductById(id: Long): ProductEntry? = + realm.query("id == $0", id).first().find() - suspend fun insert(products: List) { + suspend fun insert(productGroups: List) { realm.write { - products.forEach { copyToRealm(it, UpdatePolicy.ALL) } + productGroups.forEach { copyToRealm(it, UpdatePolicy.ALL) } } } suspend fun deleteForEvent(eventId: Long) { realm.write { - delete(query("eventId == $0", eventId).find()) + delete(query("eventId == $0", eventId).find()) } } suspend fun deleteOlderThan(maxAge: Duration) { val timestamp = Now().minus(maxAge).toEpochMilliseconds() realm.write { - delete(query("updatedAt <= $0", timestamp).find()) + delete(query("updatedAt <= $0", timestamp).find()) } } } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/db/model/ProductEntry.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/db/model/ProductEntry.kt index edff0ce6..d03088d9 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/db/model/ProductEntry.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/db/model/ProductEntry.kt @@ -1,5 +1,7 @@ package org.datepollsystems.waiterrobot.shared.features.order.db.model +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.toRealmList import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.PrimaryKey @@ -7,59 +9,69 @@ import kotlinx.datetime.Instant import org.datepollsystems.waiterrobot.shared.utils.Cents import org.datepollsystems.waiterrobot.shared.utils.extensions.Now -internal class ProductEntry() : RealmObject { - @PrimaryKey +internal class ProductGroupEntry constructor() : RealmObject { var id: Long? = null var name: String? = null - var price: Cents? = null - var soldOut: Boolean? = null - var updatedAt: Long = 0L var eventId: Long? = null - var allergens: RealmList? = null - var productGroup: ProductGroup? = null + var position: Int = Int.MAX_VALUE + var products: RealmList = realmListOf() + var updatedAt: Long = 0L val updated: Instant get() = Instant.fromEpochMilliseconds(updatedAt) constructor( id: Long, - eventId: Long, name: String, - price: Cents, - soldOut: Boolean, - productGroup: ProductGroup, - allergens: RealmList, + eventId: Long, + position: Int, + products: List, updatedAt: Instant = Now() ) : this() { this.id = id - this.eventId = eventId this.name = name - this.price = price - this.soldOut = soldOut - this.allergens = allergens - this.productGroup = productGroup + this.eventId = eventId + this.position = position + this.products = products.toRealmList() this.updatedAt = updatedAt.toEpochMilliseconds() } +} - internal class Allergen constructor() : RealmObject { - var id: Long? = null - var name: String? = null - var shortName: String? = null +internal class ProductEntry() : RealmObject { + @PrimaryKey + var id: Long? = null + var name: String? = null + var price: Cents? = null + var soldOut: Boolean? = null + var allergens: RealmList = realmListOf() + var position: Int = Int.MAX_VALUE - constructor(id: Long, name: String, shortName: String) : this() { - this.id = id - this.name = name - this.shortName = shortName - } + @Suppress("LongParameterList") + constructor( + id: Long, + name: String, + price: Cents, + soldOut: Boolean, + allergens: List, + position: Int, + ) : this() { + this.id = id + this.name = name + this.price = price + this.soldOut = soldOut + this.allergens = allergens.toRealmList() + this.position = position } +} - internal class ProductGroup constructor() : RealmObject { - var id: Long? = null - var name: String? = null +internal class AllergenEntry constructor() : RealmObject { + var id: Long? = null + var name: String? = null + var shortName: String? = null - constructor(id: Long, name: String) : this() { - this.id = id - this.name = name - } + constructor(id: Long, name: String, shortName: String) : this() { + this.id = id + this.name = name + this.shortName = shortName } } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/Product.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/Product.kt index 013984cb..4dd50aa9 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/Product.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/Product.kt @@ -8,4 +8,5 @@ data class Product( val price: Money, val soldOut: Boolean, val allergens: List, + val position: Int, ) diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/ProductGroup.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/ProductGroup.kt index 08f296f7..31630dd1 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/ProductGroup.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/ProductGroup.kt @@ -2,5 +2,7 @@ package org.datepollsystems.waiterrobot.shared.features.order.models data class ProductGroup( val id: Long, - val name: String + val name: String, + val position: Int, + val products: List ) diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/ProductGroupWithProducts.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/ProductGroupWithProducts.kt deleted file mode 100644 index 3a6612c9..00000000 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/models/ProductGroupWithProducts.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.datepollsystems.waiterrobot.shared.features.order.models - -data class ProductGroupWithProducts( - val group: ProductGroup, - val products: List -) diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/repository/ProductRepository.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/repository/ProductRepository.kt index 023f9bd4..83a10ca2 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/repository/ProductRepository.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/repository/ProductRepository.kt @@ -1,6 +1,5 @@ package org.datepollsystems.waiterrobot.shared.features.order.repository -import io.realm.kotlin.ext.toRealmList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.datetime.Instant @@ -8,12 +7,14 @@ import org.datepollsystems.waiterrobot.shared.core.CommonApp import org.datepollsystems.waiterrobot.shared.core.repository.AbstractRepository import org.datepollsystems.waiterrobot.shared.features.order.api.ProductApi import org.datepollsystems.waiterrobot.shared.features.order.api.models.ProductDto +import org.datepollsystems.waiterrobot.shared.features.order.api.models.ProductGroupDto import org.datepollsystems.waiterrobot.shared.features.order.db.ProductDatabase +import org.datepollsystems.waiterrobot.shared.features.order.db.model.AllergenEntry import org.datepollsystems.waiterrobot.shared.features.order.db.model.ProductEntry +import org.datepollsystems.waiterrobot.shared.features.order.db.model.ProductGroupEntry import org.datepollsystems.waiterrobot.shared.features.order.models.Allergen import org.datepollsystems.waiterrobot.shared.features.order.models.Product import org.datepollsystems.waiterrobot.shared.features.order.models.ProductGroup -import org.datepollsystems.waiterrobot.shared.features.order.models.ProductGroupWithProducts import org.datepollsystems.waiterrobot.shared.utils.cent import org.datepollsystems.waiterrobot.shared.utils.extensions.Now import org.datepollsystems.waiterrobot.shared.utils.extensions.olderThan @@ -34,29 +35,16 @@ internal class ProductRepository : AbstractRepository(), KoinComponent { } suspend fun getProductById(id: Long): Product? { - return productDb.getById(id)?.toModel() + return productDb.getProductById(id)?.toModel() ?: getProductGroups(true) // TODO limit force update - .flatMap(ProductGroupWithProducts::products) + .flatMap(ProductGroup::products) .find { it.id == id } } - suspend fun getProductGroups(forceUpdate: Boolean = false): List { + suspend fun getProductGroups(forceUpdate: Boolean = false): List { val eventId = CommonApp.settings.selectedEventId - fun Map>.mapProductGroupWithProducts( - mapper: (T) -> Product - ): List { - return this.map { (group, products) -> - ProductGroupWithProducts( - group = group, - products = products.map(mapper) - .sortedBy { it.name.lowercase() } - .sortedBy(Product::soldOut) - ) - }.sortedBy { it.group.name.lowercase() } - } - - fun loadFromDb(): List? { + fun loadFromDb(): List? { logger.i { "Fetching products from DB ..." } val dbProducts = productDb.getForEvent(eventId) logger.d { "Found ${dbProducts.count()} products in DB" } @@ -64,40 +52,38 @@ internal class ProductRepository : AbstractRepository(), KoinComponent { return if (dbProducts.isEmpty() || dbProducts.any { it.updated.olderThan(maxAge) }) { null } else { - dbProducts.groupBy { it.productGroup!!.toModel() } - .mapProductGroupWithProducts(ProductEntry::toModel) + dbProducts.map(ProductGroupEntry::toModel) } } - suspend fun loadFromApiAndStore(): List { + suspend fun loadFromApiAndStore(): List { logger.i { "Loading products from api ..." } val timestamp = Now() val apiProducts = productApi.getProducts(eventId) logger.d { "Got ${apiProducts.sumOf { it.products.count() }} products from api" } - val modelGroups = apiProducts.associate { it.id to ProductGroup(it.id, it.name) } - val entryGroups = - apiProducts.associate { it.id to ProductEntry.ProductGroup(it.id, it.name) } + val modelGroups = apiProducts.map(ProductGroupDto::toModel) + val entryGroups = apiProducts.map { it.toEntry(eventId, timestamp) } logger.i { "Remove old products from DB ..." } productDb.deleteForEvent(eventId) logger.i { "Saving products to DB ..." } - productDb.insert(apiProducts.flatMap { group -> - group.products.map { it.toEntry(eventId, entryGroups[group.id]!!, timestamp) } - }) + productDb.insert(entryGroups) - return apiProducts.associate { group -> - modelGroups[group.id]!! to group.products - }.mapProductGroupWithProducts(ProductDto::toModel) + return modelGroups } - return if (forceUpdate) { - loadFromApiAndStore() - } else { - loadFromDb() ?: loadFromApiAndStore() + val result = when (forceUpdate) { + true -> loadFromApiAndStore() + false -> loadFromDb() ?: loadFromApiAndStore() } + + return result + .filter { it.products.isNotEmpty() } // Do not show groups that do not have products at all + .sortedBy { it.name.lowercase() } // Sort groups with same position by name + .sortedBy(ProductGroup::position) } companion object { @@ -105,6 +91,16 @@ internal class ProductRepository : AbstractRepository(), KoinComponent { } } +private fun ProductGroupDto.toModel() = ProductGroup( + id = this.id, + name = this.name, + position = this.position, + products = this.products + .map(ProductDto::toModel) + .sortedBy { it.name.lowercase() } // Sort products with same position by name + .sortedBy(Product::position) +) + private fun ProductDto.toModel() = Product( id = this.id, name = this.name, @@ -113,23 +109,27 @@ private fun ProductDto.toModel() = Product( allergens = this.allergens.map { allergen -> Allergen(allergen.id, allergen.name, allergen.shortName) }, + position = this.position, ) -private fun ProductDto.toEntry( - eventId: Long, - group: ProductEntry.ProductGroup, - timestamp: Instant -) = ProductEntry( +private fun ProductGroupDto.toEntry(eventId: Long, timestamp: Instant) = ProductGroupEntry( id = this.id, + name = this.name, eventId = eventId, + position = this.position, + products = this.products.map { it.toEntry() }, + updatedAt = timestamp +) + +private fun ProductDto.toEntry() = ProductEntry( + id = this.id, name = this.name, price = this.price, soldOut = this.soldOut, - productGroup = group, allergens = this.allergens.map { - ProductEntry.Allergen(id = it.id, name = it.name, shortName = it.shortName) - }.toRealmList(), - updatedAt = timestamp + AllergenEntry(id = it.id, name = it.name, shortName = it.shortName) + }, + position = this.position, ) private fun ProductEntry.toModel() = Product( @@ -137,10 +137,13 @@ private fun ProductEntry.toModel() = Product( name = this.name!!, price = this.price!!.cent, soldOut = this.soldOut!!, - allergens = this.allergens!!.map { Allergen(it.id!!, it.name!!, it.shortName!!) }, + allergens = this.allergens.map { Allergen(it.id!!, it.name!!, it.shortName!!) }, + position = this.position ) -private fun ProductEntry.ProductGroup.toModel() = ProductGroup( +private fun ProductGroupEntry.toModel() = ProductGroup( id = this.id!!, - name = this.name!! + name = this.name!!, + position = this.position, + products = this.products.map(ProductEntry::toModel) ) diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderState.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderState.kt index 0efe2b95..e73c5557 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderState.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderState.kt @@ -3,12 +3,13 @@ package org.datepollsystems.waiterrobot.shared.features.order.viewmodel import org.datepollsystems.waiterrobot.shared.core.viewmodel.ViewModelState import org.datepollsystems.waiterrobot.shared.core.viewmodel.ViewState import org.datepollsystems.waiterrobot.shared.features.order.models.OrderItem -import org.datepollsystems.waiterrobot.shared.features.order.models.ProductGroupWithProducts +import org.datepollsystems.waiterrobot.shared.features.order.models.ProductGroup data class OrderState( override val viewState: ViewState = ViewState.Idle, val showConfirmationDialog: Boolean = false, - val productGroups: List = emptyList(), + val productGroups: List = emptyList(), + @Suppress("ConstructorParameterNaming") internal val _currentOrder: Map = emptyMap() // Product ID to Order ) : ViewModelState() { // Expose only as a list of OrderItems diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderViewModel.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderViewModel.kt index 670982f6..2a106db0 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderViewModel.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/order/viewmodel/OrderViewModel.kt @@ -6,8 +6,6 @@ import org.datepollsystems.waiterrobot.shared.core.viewmodel.AbstractViewModel import org.datepollsystems.waiterrobot.shared.core.viewmodel.ViewState import org.datepollsystems.waiterrobot.shared.features.order.models.OrderItem import org.datepollsystems.waiterrobot.shared.features.order.models.Product -import org.datepollsystems.waiterrobot.shared.features.order.models.ProductGroup -import org.datepollsystems.waiterrobot.shared.features.order.models.ProductGroupWithProducts import org.datepollsystems.waiterrobot.shared.features.order.repository.OrderRepository import org.datepollsystems.waiterrobot.shared.features.order.repository.ProductRepository import org.datepollsystems.waiterrobot.shared.features.table.models.Table @@ -17,6 +15,7 @@ import org.datepollsystems.waiterrobot.shared.utils.extensions.emptyToNull import org.orbitmvi.orbit.syntax.simple.intent import org.orbitmvi.orbit.syntax.simple.reduce +@Suppress("TooManyFunctions") class OrderViewModel internal constructor( private val productRepository: ProductRepository, private val orderRepository: OrderRepository, @@ -40,7 +39,7 @@ class OrderViewModel internal constructor( addItem(id, amount) { productRepository.getProductById(id) } fun addItem(product: Product, amount: Int) = - addItem(product.id, amount) { product } + addItem(product.id, amount) { productRepository.getProductById(product.id) } fun addItemNote(item: OrderItem, note: String?) = intent { @Suppress("NAME_SHADOWING") @@ -76,6 +75,7 @@ class OrderViewModel internal constructor( } } + @Suppress("MemberVisibilityCanBePrivate") // used on iOS fun removeAllOfProduct(productId: Long) = intent { reduce { state.copy(_currentOrder = state._currentOrder.minus(productId)) } } @@ -89,7 +89,8 @@ class OrderViewModel internal constructor( } fun abortOrder() = intent { - // Hide the confirmation dialog before navigation away, as otherwise on iOS it would be still shown on the new screen + // Hide the confirmation dialog before navigation away, + // as otherwise on iOS it would be still shown on the new screen reduce { state.copy(showConfirmationDialog = false) } navigator.pop() } @@ -140,13 +141,11 @@ class OrderViewModel internal constructor( } else { reduce { state.copy( - productGroups = allProducts.map { (group: ProductGroup, products: List) -> - ProductGroupWithProducts( - group = group, - products = products.filter { - it.name.contains(filter, ignoreCase = true) - } - ) + productGroups = allProducts.map { group -> + val filteredProducts = group.products + .filter { it.name.contains(filter, ignoreCase = true) } + // Also add groups with no products so that the tabs do not change + group.copy(products = filteredProducts) }, ) } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/switchevent/api/models/EventDto.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/switchevent/api/models/EventResponseDto.kt similarity index 100% rename from shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/switchevent/api/models/EventDto.kt rename to shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/switchevent/api/models/EventResponseDto.kt diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/table/api/models/TableDto.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/table/api/models/TableResponseDto.kt similarity index 100% rename from shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/table/api/models/TableDto.kt rename to shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/features/table/api/models/TableResponseDto.kt diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/root/RootViewModel.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/root/RootViewModel.kt index ad25fa8e..7965a62d 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/root/RootViewModel.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/root/RootViewModel.kt @@ -62,7 +62,7 @@ class RootViewModel internal constructor( } } reduce { state.withViewState(ViewState.Idle) } - } catch (e: ApiException.CredentialsIncorrect) { + } catch (_: ApiException.CredentialsIncorrect) { reduceError(L.root.invalidLoginLink.title(), L.root.invalidLoginLink.desc()) } } diff --git a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/Money.kt b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/Money.kt index ea85691b..8e4dd6ac 100644 --- a/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/Money.kt +++ b/shared/src/commonMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/Money.kt @@ -1,3 +1,5 @@ +@file:Suppress("MagicNumber") + package org.datepollsystems.waiterrobot.shared.utils import kotlin.math.abs diff --git a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/VersionChecker.kt b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/VersionChecker.kt index 76601c82..4a82b1eb 100644 --- a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/VersionChecker.kt +++ b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/VersionChecker.kt @@ -14,6 +14,7 @@ import kotlinx.datetime.until import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import org.datepollsystems.waiterrobot.shared.core.CommonApp.MIN_UPDATE_INFO_HOURS import org.datepollsystems.waiterrobot.shared.utils.extensions.defaultOnNull import org.koin.core.component.KoinComponent import org.koin.core.component.get @@ -56,13 +57,14 @@ object VersionChecker : KoinComponent { CommonApp.settings.lastUpdateAvailableNote .defaultOnNull(Instant.DISTANT_PAST) .until(Clock.System.now(), DateTimeUnit.HOUR) + logger.i( "New app version is available, hoursSinceLastUpdateAvailableNote: " + hoursSinceLastUpdateAvailableNote ) // Show max once a day - if (hoursSinceLastUpdateAvailableNote > 24) { + if (hoursSinceLastUpdateAvailableNote > MIN_UPDATE_INFO_HOURS) { onNewVersionAvailable() CommonApp.settings.lastUpdateAvailableNote = Clock.System.now() } diff --git a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/Koin.kt b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/Koin.kt index ea49d97d..8ef47f00 100644 --- a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/Koin.kt +++ b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/Koin.kt @@ -19,7 +19,7 @@ import org.koin.core.parameter.parametersOf @Suppress("unused") // Only used by iOS fun initKoinIos() = initKoin() -@Suppress("unused") // Only used by iOS +@Suppress("unused", "TooManyFunctions") // Only used by iOS object IosKoinComponent : KoinComponent { fun logger(tag: String): Logger = get { parametersOf(tag) } fun rootVM() = get() diff --git a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KoinSharedViewModel.kt b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KoinSharedViewModel.kt index 051f4826..362834bb 100644 --- a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KoinSharedViewModel.kt +++ b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/core/di/KoinSharedViewModel.kt @@ -6,7 +6,6 @@ import org.koin.core.definition.KoinDefinition import org.koin.core.module.Module import org.koin.core.qualifier.Qualifier - internal actual inline fun Module.sharedViewModel( qualifier: Qualifier?, noinline definition: Definition diff --git a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/FlowUtils.kt b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/FlowUtils.kt index 4a253ba5..12e63aa7 100644 --- a/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/FlowUtils.kt +++ b/shared/src/iosMain/kotlin/org/datepollsystems/waiterrobot/shared/utils/FlowUtils.kt @@ -16,7 +16,8 @@ import kotlinx.coroutines.flow.* * Collects the flow and calls the [onEach] function for each received item. * KotlinFlow -> Callback base API * - * This is needed as from Swift no coroutines can be launched and to collect a flow a coroutine (or suspendable function) is needed. + * This is needed as from Swift no coroutines can be launched and to collect a flow a coroutine + * (or suspendable function) is needed. */ @Suppress("unused") // Used by iOS fun Flow.subscribe(