From a45043424e7200c90b4b29fe6d6a74f51caf4be2 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sun, 25 Jun 2023 13:31:54 +0200 Subject: [PATCH 01/28] Use act runner --- .drone.yml | 89 ------------------------------ .gitea/workflows/buildAndroid.yaml | 16 ++++++ 2 files changed, 16 insertions(+), 89 deletions(-) delete mode 100644 .drone.yml create mode 100644 .gitea/workflows/buildAndroid.yaml diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 195de40..0000000 --- a/.drone.yml +++ /dev/null @@ -1,89 +0,0 @@ ---- -kind: pipeline -type: docker -name: Android - -steps: -- name: prepare signing - image: busybox - environment: - STOREPASSWORD: - from_secret: StorePassword - KEYPASSWORD: - from_secret: KeyPassword - commands: - - touch keystore.properties - - echo "storePassword=$STOREPASSWORD" >> keystore.properties - - echo "keyPassword=$KEYPASSWORD" >> keystore.properties - - echo "keyAlias=key0" >> keystore.properties - - echo "storeFile=../AndroidKey" >> keystore.properties - -- name: generate versionCode - image: busybox - commands: - - touch version.properties - - let timestamp=$(date +%s)/10 - - echo "versionCode=$timestamp" >> version.properties - -- name: build - image: mingc/android-build-box - commands: - - ./gradlew test - - ./gradlew assembleRelease - - ./gradlew bundleRelease - -- name: upload latest apk - image: vividboarder/drone-webdav - settings: - file: app/build/outputs/apk/release/app-release.apk - destination: https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.apk - username: - from_secret: NextCloudUser - password: - from_secret: NextCloudPassword - -- name: upload latest bundle - image: vividboarder/drone-webdav - settings: - file: app/build/outputs/bundle/release/app-release.aab - destination: https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.aab - username: - from_secret: NextCloudUser - password: - from_secret: NextCloudPassword - -- name: upload tagged apk - image: vividboarder/drone-webdav - settings: - file: app/build/outputs/apk/release/app-release.apk - destination: 'https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/tagged/app-release$DRONE_TAG.apk' - username: - from_secret: NextCloudUser - password: - from_secret: NextCloudPassword - when: - event: - - tag - -- name: upload tagged bundle - image: vividboarder/drone-webdav - settings: - file: app/build/outputs/bundle/release/app-release.aab - destination: 'https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/tagged/app-release$DRONE_TAG.aab' - username: - from_secret: NextCloudUser - password: - from_secret: NextCloudPassword - when: - event: - - tag - -- name: slack notification - image: plugins/slack - settings: - webhook: - from_secret: SlackWebhook - when: - status: - - failure - - success \ No newline at end of file diff --git a/.gitea/workflows/buildAndroid.yaml b/.gitea/workflows/buildAndroid.yaml new file mode 100644 index 0000000..d9e3168 --- /dev/null +++ b/.gitea/workflows/buildAndroid.yaml @@ -0,0 +1,16 @@ +name: Build Android +on: [pull_request, push] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v2 + - name: set up JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + - name: Build the app + run: ./gradlew build \ No newline at end of file From 81c540c2a5d8b147930d515da8dd5a5eb7886354 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sun, 25 Jun 2023 19:51:40 +0200 Subject: [PATCH 02/28] Add signing and version code. --- .gitea/workflows/buildAndroid.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.gitea/workflows/buildAndroid.yaml b/.gitea/workflows/buildAndroid.yaml index d9e3168..a40e301 100644 --- a/.gitea/workflows/buildAndroid.yaml +++ b/.gitea/workflows/buildAndroid.yaml @@ -6,11 +6,27 @@ jobs: steps: - name: Checkout the code uses: actions/checkout@v2 + - name: set up JDK uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' cache: gradle + + - name: Prepare signing + run: | + touch keystore.properties + echo "storePassword=$STOREPASSWORD" >> keystore.properties + echo "keyPassword=$KEYPASSWORD" >> keystore.properties + echo "keyAlias=key0" >> keystore.properties + echo "storeFile=../AndroidKey" >> keystore.properties + + - name: Generate versionCode + run: | + touch version.properties + let timestamp=$(date +%s)/10 + echo "versionCode=$timestamp" >> version.properties + - name: Build the app run: ./gradlew build \ No newline at end of file From 4f87321e2cf0b54de44c755deb7116cc7e6dd520 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Wed, 28 Jun 2023 17:25:48 +0200 Subject: [PATCH 03/28] Use android build box iamge. --- .gitea/workflows/buildAndroid.yaml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/buildAndroid.yaml b/.gitea/workflows/buildAndroid.yaml index a40e301..8b98d59 100644 --- a/.gitea/workflows/buildAndroid.yaml +++ b/.gitea/workflows/buildAndroid.yaml @@ -2,23 +2,16 @@ name: Build Android on: [pull_request, push] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-latest-android steps: - name: Checkout the code uses: actions/checkout@v2 - - name: set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - name: Prepare signing run: | touch keystore.properties - echo "storePassword=$STOREPASSWORD" >> keystore.properties - echo "keyPassword=$KEYPASSWORD" >> keystore.properties + echo "storePassword=${{ secrets.STOREPASSWORD }}" >> keystore.properties + echo "keyPassword=${{ secrets.KEYPASSWORD }}" >> keystore.properties echo "keyAlias=key0" >> keystore.properties echo "storeFile=../AndroidKey" >> keystore.properties @@ -28,5 +21,11 @@ jobs: let timestamp=$(date +%s)/10 echo "versionCode=$timestamp" >> version.properties - - name: Build the app - run: ./gradlew build \ No newline at end of file + - name: Test the app + run: ./gradlew test + + - name: Build apk + run: ./gradlew assembleRelease + + - name: Build abb + run: ./gradlew bundleRelease \ No newline at end of file From ceebe92f8bb8f0e0ee5bad9cac7d73285f86e92f Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 30 Jun 2023 10:47:19 +0200 Subject: [PATCH 04/28] Deploy latest to nextcloud. --- .gitea/workflows/buildAndroid.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/buildAndroid.yaml b/.gitea/workflows/buildAndroid.yaml index 8b98d59..3871a80 100644 --- a/.gitea/workflows/buildAndroid.yaml +++ b/.gitea/workflows/buildAndroid.yaml @@ -28,4 +28,10 @@ jobs: run: ./gradlew assembleRelease - name: Build abb - run: ./gradlew bundleRelease \ No newline at end of file + run: ./gradlew bundleRelease + + - name: Deploy latest to Nextcloud + run: | + curl -k -u "${{ secrets.NEXTCLOUD_USERNAME }}:${{ secrets.NEXTCLOUD_PASSWORD }}" -T "app/build/outputs/apk/release/app-release.apk" "https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.apk" + curl -k -u "${{ secrets.NEXTCLOUD_USERNAME }}:${{ secrets.NEXTCLOUD_PASSWORD }}" -T "app/build/outputs/bundle/release/app-release.aab" "https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.aab" + From add100146d3e49564281719a25d847f5cc39bc64 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 30 Jun 2023 12:03:03 +0200 Subject: [PATCH 05/28] Notify on slack. --- .gitea/workflows/buildAndroid.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/buildAndroid.yaml b/.gitea/workflows/buildAndroid.yaml index 3871a80..12b8111 100644 --- a/.gitea/workflows/buildAndroid.yaml +++ b/.gitea/workflows/buildAndroid.yaml @@ -35,3 +35,9 @@ jobs: curl -k -u "${{ secrets.NEXTCLOUD_USERNAME }}:${{ secrets.NEXTCLOUD_PASSWORD }}" -T "app/build/outputs/apk/release/app-release.apk" "https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.apk" curl -k -u "${{ secrets.NEXTCLOUD_USERNAME }}:${{ secrets.NEXTCLOUD_PASSWORD }}" -T "app/build/outputs/bundle/release/app-release.aab" "https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.aab" + - uses: https://github.com/ravsamhq/notify-slack-action@v2 + if: always() + with: + status: ${{ job.status }} # required + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required \ No newline at end of file From 217370079da0a21c4c477012b3dbab53b7d4aa00 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 30 Jun 2023 12:29:32 +0200 Subject: [PATCH 06/28] Publish tagged builds. --- .gitea/workflows/buildAndroid.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitea/workflows/buildAndroid.yaml b/.gitea/workflows/buildAndroid.yaml index 12b8111..501fc60 100644 --- a/.gitea/workflows/buildAndroid.yaml +++ b/.gitea/workflows/buildAndroid.yaml @@ -35,6 +35,11 @@ jobs: curl -k -u "${{ secrets.NEXTCLOUD_USERNAME }}:${{ secrets.NEXTCLOUD_PASSWORD }}" -T "app/build/outputs/apk/release/app-release.apk" "https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.apk" curl -k -u "${{ secrets.NEXTCLOUD_USERNAME }}:${{ secrets.NEXTCLOUD_PASSWORD }}" -T "app/build/outputs/bundle/release/app-release.aab" "https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/latest/app-release.aab" + - name: Deploy tagged build to Nextcloud + run: | + curl -k -u "${{ secrets.NEXTCLOUD_USERNAME }}:${{ secrets.NEXTCLOUD_PASSWORD }}" -T "app/build/outputs/apk/release/app-release.apk" "https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/tagged/app-release-${GITHUB_REF_NAME##*/}.apk" + curl -k -u "${{ secrets.NEXTCLOUD_USERNAME }}:${{ secrets.NEXTCLOUD_PASSWORD }}" -T "app/build/outputs/bundle/release/app-release.aab" "https://nextcloud.zobrist.me/remote.php/dav/files/deploy/TichuCounter/tagged/app-release-${GITHUB_REF_NAME##*/}.aab" + - uses: https://github.com/ravsamhq/notify-slack-action@v2 if: always() with: From 07219bffb42ce61c9f7f434601fb60d6a50f0071 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 19 Aug 2023 16:05:41 +0200 Subject: [PATCH 07/28] Update dependencies. --- app/build.gradle | 7 +++---- build.gradle | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 663caf8..d5be58c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,7 +24,7 @@ versionProperties.load(new FileInputStream(versionPropertiesFile)) android { - compileSdkVersion 33 + compileSdk 33 defaultConfig { applicationId "me.zobrist.tichucounter" @@ -63,12 +63,11 @@ android { } buildFeatures { - viewBinding = true compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.7" + kotlinCompilerExtensionVersion = "1.4.8" } compileOptions { @@ -91,7 +90,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation "androidx.compose.material3:material3:1.1.0" + implementation "androidx.compose.material3:material3:1.1.1" implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.code.gson:gson:2.9.0' diff --git a/build.gradle b/build.gradle index 24af24d..dc84979 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.8.21" + ext.kotlin_version = "1.8.22" repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From ec3b51051a8ecf44a4dc62339bd58f059e00a719 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 19 Aug 2023 17:50:41 +0200 Subject: [PATCH 08/28] Add swipe to open and delete to history items. --- .../me/zobrist/tichucounter/MainActivity.kt | 43 ++++- .../tichucounter/ui/counter/KeyboardView.kt | 31 +++- .../tichucounter/ui/counter/TeamNamesView.kt | 9 +- .../tichucounter/ui/history/HistoryView.kt | 175 ++++++++++-------- .../tichucounter/ui/layout/DrawerContent.kt | 22 ++- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 185 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index 0aacbf2..fdb1e87 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -6,12 +6,30 @@ import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.outlined.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.outlined.Calculate +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Keyboard +import androidx.compose.material.icons.outlined.List +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.outlined.Redo +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.Undo +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalNavigationDrawer +import androidx.compose.material3.Scaffold +import androidx.compose.material3.rememberDrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.NavHostController @@ -22,12 +40,23 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import me.zobrist.tichucounter.domain.* +import me.zobrist.tichucounter.domain.DrawerItem +import me.zobrist.tichucounter.domain.ISettingsChangeListener +import me.zobrist.tichucounter.domain.KeepScreenOn +import me.zobrist.tichucounter.domain.Language +import me.zobrist.tichucounter.domain.Route +import me.zobrist.tichucounter.domain.SettingsAdapter +import me.zobrist.tichucounter.domain.Theme +import me.zobrist.tichucounter.domain.TopBarAction +import me.zobrist.tichucounter.domain.TopBarState +import me.zobrist.tichucounter.domain.composable +import me.zobrist.tichucounter.domain.navigate import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.MainViewModel import me.zobrist.tichucounter.ui.about.AboutView import me.zobrist.tichucounter.ui.composables.DropDownMenu -import me.zobrist.tichucounter.ui.counter.* +import me.zobrist.tichucounter.ui.counter.Counter +import me.zobrist.tichucounter.ui.counter.CounterViewModel import me.zobrist.tichucounter.ui.history.HistoryList import me.zobrist.tichucounter.ui.history.HistoryViewModel import me.zobrist.tichucounter.ui.layout.DrawerContent diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt index 3ab3407..32fd620 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt @@ -1,17 +1,40 @@ package me.zobrist.tichucounter.ui.counter import android.content.res.Configuration -import androidx.compose.animation.core.* +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Backspace import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.KeyboardHide import androidx.compose.material.icons.outlined.SwapHoriz -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.Divider +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt index 9527c73..2e911a9 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/TeamNamesView.kt @@ -1,9 +1,12 @@ package me.zobrist.tichucounter.ui.counter import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index fa6eabe..e0d40b6 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -1,34 +1,45 @@ package me.zobrist.tichucounter.ui.history -import androidx.compose.foundation.clickable +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DeleteForever -import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.outlined.RestartAlt import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissValue +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberDismissState 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.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -38,7 +49,6 @@ import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.domain.getTotalPoints -import me.zobrist.tichucounter.ui.composables.DropDownMenu import java.text.DateFormat import java.util.Date import java.util.Locale @@ -46,8 +56,7 @@ import java.util.Locale @Composable fun HistoryList( - viewModel: HistoryViewModel, - navigateToCalculator: () -> Unit + viewModel: HistoryViewModel, navigateToCalculator: () -> Unit ) { var showDeleteDialog by remember { mutableStateOf(false) } @@ -78,14 +87,12 @@ fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {} AlertDialog( onDismissRequest = { onExecuted(false) }, dismissButton = { - TextButton({ onExecuted(false) }) - { + TextButton({ onExecuted(false) }) { Text(stringResource(R.string.cancel)) } }, confirmButton = { - TextButton({ onExecuted(true) }) - { + TextButton({ onExecuted(true) }) { Text(stringResource(R.string.ok)) } }, @@ -98,8 +105,8 @@ fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {} @Composable fun HistoryList( games: List, - onOpenClicked: (GameId: Long) -> Unit, - onDeleteClicked: (GameId: Long) -> Unit, + onOpenClicked: (gameId: Long) -> Unit, + onDeleteClicked: (gameId: Long) -> Unit, onDeleteAllClicked: () -> Unit ) { @@ -113,7 +120,7 @@ fun HistoryList( ) } items(games.filter { it.game.active }) { - HistoryListItem(it, onOpenClicked, onDeleteClicked) + HistoryListItem(it) } if (games.count() > 1) { @@ -125,13 +132,14 @@ fun HistoryList( ) } - items(games.filter { !it.game.active }) { - HistoryListItem(it, onOpenClicked, onDeleteClicked) + items(items = games.filter { !it.game.active }, key = { + it.hashCode() + }) { + DismissibleHistoryListItem(it, onOpenClicked, onDeleteClicked) } item { - Button( - enabled = games.count() > 1, + Button(enabled = games.count() > 1, modifier = Modifier .padding(start = 4.dp, end = 4.dp, top = 10.dp) .align(CenterVertically) @@ -146,11 +154,70 @@ fun HistoryList( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DismissibleHistoryListItem( + game: GameWithScores, + onOpenClicked: (gameId: Long) -> Unit, + onDeleteClicked: (gameId: Long) -> Unit +) { + val dismissState = rememberDismissState(confirmValueChange = { + if (it == DismissValue.DismissedToStart) { + onDeleteClicked(game.game.uid) + } + if (it == DismissValue.DismissedToEnd) { + onOpenClicked(game.game.uid) + } + true + }) + + SwipeToDismiss(state = dismissState, background = { + val direction = dismissState.dismissDirection ?: return@SwipeToDismiss + val color by animateColorAsState( + when (dismissState.targetValue) { + DismissValue.DismissedToStart -> Color.Red + else -> MaterialTheme.colorScheme.background + + }, label = "" + ) + val alignment = when (direction) { + DismissDirection.StartToEnd -> Alignment.CenterStart + DismissDirection.EndToStart -> Alignment.CenterEnd + } + val icon = when (direction) { + DismissDirection.StartToEnd -> Icons.Outlined.RestartAlt + DismissDirection.EndToStart -> Icons.Outlined.Delete + } + val text = when (direction) { + DismissDirection.StartToEnd -> stringResource(id = R.string.activate) + DismissDirection.EndToStart -> stringResource(id = R.string.delete) + } + val scale by animateFloatAsState( + if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f, label = "" + ) + + Box( + Modifier + .fillMaxSize() + .background(color) + .padding(horizontal = 20.dp), + contentAlignment = alignment + ) { + Row { + Icon( + icon, contentDescription = null, modifier = Modifier.scale(scale) + ) + Text(text = text) + } + } + }, dismissContent = { + HistoryListItem(game = game) + }) +} + @Composable fun HistoryListItem( - game: GameWithScores, - onOpenClicked: (GameId: Long) -> Unit, - onDeleteClicked: (GameId: Long) -> Unit + game: GameWithScores ) { val format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) @@ -168,13 +235,10 @@ fun HistoryListItem( Card( modifier = Modifier .fillMaxWidth() - .padding(all = 4.dp) - .clickable { onOpenClicked(game.game.uid) }, - colors = cardColor + .padding(all = 4.dp), colors = cardColor ) { Row( - Modifier - .padding(all = 12.dp) + Modifier.padding(all = 12.dp) ) { Column(Modifier.weight(4f)) { Text( @@ -193,38 +257,6 @@ fun HistoryListItem( style = MaterialTheme.typography.labelSmall ) } - Column( - Modifier - .wrapContentSize() - .width(40.dp) - ) { - - if (!game.game.active) { - var expanded by remember { mutableStateOf(false) } - - Icon( - modifier = Modifier - .padding(start = 20.dp, bottom = 20.dp) - .clickable { expanded = true }, - imageVector = Icons.Outlined.MoreVert, - contentDescription = null - ) - - - DropDownMenu( - mapOf("delete" to R.string.delete), - "", - expanded, - ) { - expanded = false - it?.let { - when (it) { - "delete" -> onDeleteClicked(game.game.uid) - } - } - } - } - } } } } @@ -234,24 +266,15 @@ fun HistoryListItem( private fun HistoryListPreview() { val tempData = listOf( GameWithScores( - Game(true, "abc", "def", Date(), Date()), - listOf(Round(1, 550, 500)) - ), - GameWithScores( - Game(false, "ADTH", "dogfg", Date(), Date()), - listOf(Round(2, 20, 60)) - ), - GameWithScores( - Game(false, "TeamA3 langer Name", "TeamB3", Date(), Date()), - listOf(Round(3, 30, 70)) - ), - GameWithScores( - Game(false, "TeamA4", "TeamB4", Date(), Date()), - listOf(Round(4, 40, 80)) - ), - GameWithScores( - Game(false, "TeamA5", "TeamB5", Date(), Date()), - listOf(Round(5, 50, 90)) + Game(true, "abc", "def", Date(), Date()), listOf(Round(1, 550, 500)) + ), GameWithScores( + Game(false, "ADTH", "dogfg", Date(), Date()), listOf(Round(2, 20, 60)) + ), GameWithScores( + Game(false, "TeamA3 langer Name", "TeamB3", Date(), Date()), listOf(Round(3, 30, 70)) + ), GameWithScores( + Game(false, "TeamA4", "TeamB4", Date(), Date()), listOf(Round(4, 40, 80)) + ), GameWithScores( + Game(false, "TeamA5", "TeamB5", Date(), Date()), listOf(Round(5, 50, 90)) ) ) HistoryList(tempData, {}, {}) {} diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt b/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt index 1dd1b55..eaf1f43 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/layout/DrawerContent.kt @@ -1,20 +1,28 @@ package me.zobrist.tichucounter.ui.layout import android.content.res.Configuration -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.outlined.* -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.outlined.Calculate +import androidx.compose.material.icons.outlined.List +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import me.zobrist.tichucounter.R -import me.zobrist.tichucounter.domain.* +import me.zobrist.tichucounter.domain.DrawerItem +import me.zobrist.tichucounter.domain.Route import me.zobrist.tichucounter.ui.AppTheme -import me.zobrist.tichucounter.ui.counter.* @Composable fun DrawerContent( diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ae3d8f6..bee00f7 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -24,5 +24,6 @@ Counter About Schreib uns + Aktivieren \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48012ba..65e0d14 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,4 +28,5 @@ About Contact us Play Store + Activate \ No newline at end of file From c8098fc90417c1e42406dfa87267b297b51f752f Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sun, 20 Aug 2023 12:31:20 +0200 Subject: [PATCH 09/28] Increase dismiss threshold. Change translation of continue. --- .../zobrist/tichucounter/ui/history/HistoryView.kt | 13 ++++++++++--- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index e0d40b6..97ae917 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -4,6 +4,7 @@ package me.zobrist.tichucounter.ui.history import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -40,6 +41,7 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -161,7 +163,11 @@ fun DismissibleHistoryListItem( onOpenClicked: (gameId: Long) -> Unit, onDeleteClicked: (gameId: Long) -> Unit ) { - val dismissState = rememberDismissState(confirmValueChange = { + + val density = LocalDensity.current + val dismissState = rememberDismissState( + positionalThreshold = { with(density) { 100.dp.toPx() } }, + confirmValueChange = { if (it == DismissValue.DismissedToStart) { onDeleteClicked(game.game.uid) } @@ -189,7 +195,7 @@ fun DismissibleHistoryListItem( DismissDirection.EndToStart -> Icons.Outlined.Delete } val text = when (direction) { - DismissDirection.StartToEnd -> stringResource(id = R.string.activate) + DismissDirection.StartToEnd -> stringResource(id = R.string.continue_play) DismissDirection.EndToStart -> stringResource(id = R.string.delete) } val scale by animateFloatAsState( @@ -203,7 +209,8 @@ fun DismissibleHistoryListItem( .padding(horizontal = 20.dp), contentAlignment = alignment ) { - Row { + Column(verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally) { Icon( icon, contentDescription = null, modifier = Modifier.scale(scale) ) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bee00f7..98e1a74 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -24,6 +24,6 @@ Counter About Schreib uns - Aktivieren + Weiterspielen \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 65e0d14..d688695 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,5 +28,5 @@ About Contact us Play Store - Activate + Continue \ No newline at end of file From 55a2293b6ca1cf026d63a343d7d8696225d0cebe Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sun, 20 Aug 2023 22:20:45 +0200 Subject: [PATCH 10/28] Show snackbar with undo option. Don't jump back to calculator on history click. --- .../me/zobrist/tichucounter/MainActivity.kt | 10 +- .../tichucounter/ui/history/HistoryView.kt | 117 +++++++++++++----- .../ui/history/HistoryViewModel.kt | 13 +- app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 5 files changed, 114 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index fdb1e87..cb9e40b 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -23,6 +23,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -180,8 +182,11 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { ) { var topBarState by remember { mutableStateOf(TopBarState()) } + var snackbarHostState by remember { mutableStateOf(SnackbarHostState()) } + val scope = rememberCoroutineScope() Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, floatingActionButton = { if (showFab) { FloatingActionButton( @@ -245,7 +250,10 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { topBarState = TopBarState(title = stringResource(R.string.menu_history)) { scope.launch { drawerState.open() } } - HistoryList(historyViewModel) { navController.navigate(Route.COUNTER) } + HistoryList( + historyViewModel, + snackbarHostState + ) { navController.navigate(Route.COUNTER) } } composable(Route.SETTINGS) { topBarState = diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index 97ae917..a8e4ddb 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -3,6 +3,7 @@ package me.zobrist.tichucounter.ui.history import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -13,7 +14,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DeleteForever @@ -27,6 +30,9 @@ import androidx.compose.material3.DismissValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.SwipeToDismiss import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -35,6 +41,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically @@ -46,6 +53,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch import me.zobrist.tichucounter.R import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.entity.Game @@ -58,11 +66,15 @@ import java.util.Locale @Composable fun HistoryList( - viewModel: HistoryViewModel, navigateToCalculator: () -> Unit + viewModel: HistoryViewModel, + snackbarHostState: SnackbarHostState, + navigateToCalculator: () -> Unit, ) { - + val scope = rememberCoroutineScope() + val lazyListState = rememberLazyListState() var showDeleteDialog by remember { mutableStateOf(false) } + DeleteConfirmDialog(showDeleteDialog) { showDeleteDialog = false if (it) { @@ -70,14 +82,48 @@ fun HistoryList( } } + val deletedMessage = stringResource(id = R.string.delete_success) + val deletedActionLabel = stringResource(id = R.string.undo_question) + + val activatedMessage = stringResource(id = R.string.activated_success) + val activatedActionLabel = stringResource(id = R.string.to_calculator_question) + HistoryList( - viewModel.gameAndHistory, - { - viewModel.activateGame(it) - navigateToCalculator() + games = viewModel.gameAndHistory, + onOpenClicked = { + scope.launch { + viewModel.activateGame(it) + lazyListState.animateScrollToItem(0) + + val result = snackbarHostState.showSnackbar( + message = activatedMessage, + actionLabel = activatedActionLabel, + duration = SnackbarDuration.Short + ) + + if (result == SnackbarResult.ActionPerformed) { + navigateToCalculator() + } + } }, - { viewModel.deleteGame(it) }, - { showDeleteDialog = true }, + onDeleteClicked = { + scope.launch { + viewModel.markToDelete(it) + val result = snackbarHostState.showSnackbar( + message = deletedMessage, + actionLabel = deletedActionLabel, + duration = SnackbarDuration.Short + ) + + if (result == SnackbarResult.Dismissed) { + viewModel.deleteGame(it) + } else { + viewModel.unmarkToDelete(it) + } + } + }, + onDeleteAllClicked = { showDeleteDialog = true }, + lazyListState = lazyListState ) } @@ -104,16 +150,17 @@ fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {} } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun HistoryList( games: List, onOpenClicked: (gameId: Long) -> Unit, onDeleteClicked: (gameId: Long) -> Unit, - onDeleteAllClicked: () -> Unit - + onDeleteAllClicked: () -> Unit, + lazyListState: LazyListState = LazyListState(), ) { Row { - LazyColumn { + LazyColumn(state = lazyListState) { item { Text( modifier = Modifier.padding(start = 10.dp, end = 10.dp), @@ -122,7 +169,7 @@ fun HistoryList( ) } items(games.filter { it.game.active }) { - HistoryListItem(it) + HistoryListItem(it, Modifier.animateItemPlacement()) } if (games.count() > 1) { @@ -137,7 +184,9 @@ fun HistoryList( items(items = games.filter { !it.game.active }, key = { it.hashCode() }) { - DismissibleHistoryListItem(it, onOpenClicked, onDeleteClicked) + DismissibleHistoryListItem( + it, onOpenClicked, onDeleteClicked, Modifier.animateItemPlacement() + ) } item { @@ -145,7 +194,8 @@ fun HistoryList( modifier = Modifier .padding(start = 4.dp, end = 4.dp, top = 10.dp) .align(CenterVertically) - .fillMaxWidth(), + .fillMaxWidth() + .animateItemPlacement(), onClick = { onDeleteAllClicked() }) { Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null) Text(text = stringResource(id = R.string.deleteAll)) @@ -161,23 +211,24 @@ fun HistoryList( fun DismissibleHistoryListItem( game: GameWithScores, onOpenClicked: (gameId: Long) -> Unit, - onDeleteClicked: (gameId: Long) -> Unit + onDeleteClicked: (gameId: Long) -> Unit, + modifier: Modifier = Modifier, ) { val density = LocalDensity.current - val dismissState = rememberDismissState( - positionalThreshold = { with(density) { 100.dp.toPx() } }, - confirmValueChange = { - if (it == DismissValue.DismissedToStart) { - onDeleteClicked(game.game.uid) - } - if (it == DismissValue.DismissedToEnd) { - onOpenClicked(game.game.uid) - } - true - }) + val dismissState = + rememberDismissState(positionalThreshold = { with(density) { 100.dp.toPx() } }, + confirmValueChange = { + if (it == DismissValue.DismissedToStart) { + onDeleteClicked(game.game.uid) + } + if (it == DismissValue.DismissedToEnd) { + onOpenClicked(game.game.uid) + } + true + }) - SwipeToDismiss(state = dismissState, background = { + SwipeToDismiss(modifier = modifier, state = dismissState, background = { val direction = dismissState.dismissDirection ?: return@SwipeToDismiss val color by animateColorAsState( when (dismissState.targetValue) { @@ -209,8 +260,10 @@ fun DismissibleHistoryListItem( .padding(horizontal = 20.dp), contentAlignment = alignment ) { - Column(verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { Icon( icon, contentDescription = null, modifier = Modifier.scale(scale) ) @@ -224,7 +277,7 @@ fun DismissibleHistoryListItem( @Composable fun HistoryListItem( - game: GameWithScores + game: GameWithScores, modifier: Modifier = Modifier ) { val format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) @@ -240,7 +293,7 @@ fun HistoryListItem( val totalScores = game.getTotalPoints() Card( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(all = 4.dp), colors = cardColor ) { @@ -284,5 +337,5 @@ private fun HistoryListPreview() { Game(false, "TeamA5", "TeamB5", Date(), Date()), listOf(Round(5, 50, 90)) ) ) - HistoryList(tempData, {}, {}) {} + HistoryList(tempData, {}, {}, {}) } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt index ed3f587..9507838 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryViewModel.kt @@ -20,16 +20,27 @@ class HistoryViewModel @Inject constructor( var gameAndHistory by mutableStateOf(emptyList()) private set + private var fullList: List = emptyList() + init { viewModelScope.launch { gameRepository.getAllWithRoundFlow().collect { games -> - gameAndHistory = + fullList = games.sortedBy { it.game.modified }.sortedBy { it.game.active }.reversed() + gameAndHistory = fullList } } } + fun markToDelete(gameId: Long) { + gameAndHistory = fullList.filter { it.game.uid != gameId } + } + + fun unmarkToDelete(gameId: Long) { + gameAndHistory = fullList + } + fun deleteGame(gameId: Long) { viewModelScope.launch { gameRepository.deleteGame(gameId) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 98e1a74..d2138cd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -25,5 +25,9 @@ About Schreib uns Weiterspielen + Spiel gelöscht. + RÜCKGÄNGIG + Spiel aktiviert. + WEITERSPIELEN \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d688695..bf18678 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,4 +29,8 @@ Contact us Play Store Continue + Game deleted. + UNDO + Game activated. + CONTINUE PLAYING \ No newline at end of file From 7108af4cf43c31c0c9cf037e18233aca41e88738 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sun, 20 Aug 2023 22:22:33 +0200 Subject: [PATCH 11/28] Fix typo. --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf18678..3d18461 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,7 +17,7 @@ Off New Game Delete history - You really want to delete the the history? This action can\'t be undone. + You really want to delete the history? This action can\'t be undone. Cancel OK Delete From 8521247c58d7b53a2580a4f4fd531ddeb3eeb88b Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 25 Aug 2023 15:59:58 +0200 Subject: [PATCH 12/28] Show all games in list but mark active game with an badge. --- .../tichucounter/ui/history/HistoryView.kt | 220 +++++++++--------- 1 file changed, 116 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index a8e4ddb..8f2131b 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -10,9 +10,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items @@ -22,9 +25,9 @@ import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.DeleteForever import androidx.compose.material.icons.outlined.RestartAlt import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Badge import androidx.compose.material3.Button import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.DismissDirection import androidx.compose.material3.DismissValue import androidx.compose.material3.ExperimentalMaterial3Api @@ -45,6 +48,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color @@ -161,58 +165,49 @@ fun HistoryList( ) { Row { LazyColumn(state = lazyListState) { - item { - Text( - modifier = Modifier.padding(start = 10.dp, end = 10.dp), - text = stringResource(R.string.active), - style = MaterialTheme.typography.headlineSmall - ) - } - items(games.filter { it.game.active }) { - HistoryListItem(it, Modifier.animateItemPlacement()) - } - - if (games.count() > 1) { - item { - Text( - modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp), - text = stringResource(R.string.inactive), - style = MaterialTheme.typography.headlineSmall + items( + items = games, + key = { it.hashCode() }) { + if (it.game.active) { + HistoryListItem( + it, + Modifier.animateItemPlacement() ) - } - - items(items = games.filter { !it.game.active }, key = { - it.hashCode() - }) { + } else { DismissibleHistoryListItem( - it, onOpenClicked, onDeleteClicked, Modifier.animateItemPlacement() + it, + Modifier.animateItemPlacement(), + onOpenClicked, + onDeleteClicked ) } + } - item { - Button(enabled = games.count() > 1, - modifier = Modifier - .padding(start = 4.dp, end = 4.dp, top = 10.dp) - .align(CenterVertically) - .fillMaxWidth() - .animateItemPlacement(), - onClick = { onDeleteAllClicked() }) { - Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null) - Text(text = stringResource(id = R.string.deleteAll)) - } + item { + Button(enabled = games.count() > 1, + modifier = Modifier + .padding(start = 4.dp, end = 4.dp, top = 10.dp) + .align(CenterVertically) + .fillMaxWidth() + .animateItemPlacement(), + onClick = { onDeleteAllClicked() }) { + Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null) + Text(text = stringResource(id = R.string.deleteAll)) } } } } + + } @OptIn(ExperimentalMaterial3Api::class) @Composable fun DismissibleHistoryListItem( game: GameWithScores, + modifier: Modifier = Modifier, onOpenClicked: (gameId: Long) -> Unit, onDeleteClicked: (gameId: Long) -> Unit, - modifier: Modifier = Modifier, ) { val density = LocalDensity.current @@ -228,53 +223,65 @@ fun DismissibleHistoryListItem( true }) - SwipeToDismiss(modifier = modifier, state = dismissState, background = { - val direction = dismissState.dismissDirection ?: return@SwipeToDismiss - val color by animateColorAsState( - when (dismissState.targetValue) { - DismissValue.DismissedToStart -> Color.Red - else -> MaterialTheme.colorScheme.background + val directions = if (game.game.active) { + setOf() - }, label = "" - ) - val alignment = when (direction) { - DismissDirection.StartToEnd -> Alignment.CenterStart - DismissDirection.EndToStart -> Alignment.CenterEnd - } - val icon = when (direction) { - DismissDirection.StartToEnd -> Icons.Outlined.RestartAlt - DismissDirection.EndToStart -> Icons.Outlined.Delete - } - val text = when (direction) { - DismissDirection.StartToEnd -> stringResource(id = R.string.continue_play) - DismissDirection.EndToStart -> stringResource(id = R.string.delete) - } - val scale by animateFloatAsState( - if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f, label = "" - ) + } else { + setOf(DismissDirection.EndToStart, DismissDirection.StartToEnd) + } - Box( - Modifier - .fillMaxSize() - .background(color) - .padding(horizontal = 20.dp), - contentAlignment = alignment - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Icon( - icon, contentDescription = null, modifier = Modifier.scale(scale) - ) - Text(text = text) + SwipeToDismiss( + modifier = modifier, + state = dismissState, + directions = directions, + background = { + val direction = dismissState.dismissDirection ?: return@SwipeToDismiss + val color by animateColorAsState( + when (dismissState.targetValue) { + DismissValue.DismissedToStart -> Color.Red + else -> MaterialTheme.colorScheme.background + + }, label = "" + ) + val alignment = when (direction) { + DismissDirection.StartToEnd -> Alignment.CenterStart + DismissDirection.EndToStart -> Alignment.CenterEnd } - } - }, dismissContent = { - HistoryListItem(game = game) - }) + val icon = when (direction) { + DismissDirection.StartToEnd -> Icons.Outlined.RestartAlt + DismissDirection.EndToStart -> Icons.Outlined.Delete + } + val text = when (direction) { + DismissDirection.StartToEnd -> stringResource(id = R.string.continue_play) + DismissDirection.EndToStart -> stringResource(id = R.string.delete) + } + val scale by animateFloatAsState( + if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f, label = "" + ) + + Box( + Modifier + .fillMaxSize() + .background(color) + .padding(horizontal = 20.dp), + contentAlignment = alignment + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + icon, contentDescription = null, modifier = Modifier.scale(scale) + ) + Text(text = text) + } + } + }, dismissContent = { + HistoryListItem(game = game) + }) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HistoryListItem( game: GameWithScores, modifier: Modifier = Modifier @@ -282,40 +289,45 @@ fun HistoryListItem( val format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) - - val cardColor = if (game.game.active) { - CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) - - } else { - CardDefaults.cardColors() - } - val totalScores = game.getTotalPoints() Card( modifier = modifier .fillMaxWidth() - .padding(all = 4.dp), colors = cardColor + .padding(all = 4.dp) ) { Row( Modifier.padding(all = 12.dp) ) { - Column(Modifier.weight(4f)) { - Text( - text = game.game.nameA + " vs " + game.game.nameB, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.headlineSmall - ) - Text( - text = totalScores.first.toString() + " : " + totalScores.second.toString(), - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.padding(5.dp)) - Text( - text = format.format(game.game.modified), - style = MaterialTheme.typography.labelSmall - ) + Box( modifier = modifier.fillMaxSize()) { + Column { + Text( + text = game.game.nameA + " vs " + game.game.nameB, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.headlineSmall + ) + Text( + text = totalScores.first.toString() + " : " + totalScores.second.toString(), + style = MaterialTheme.typography.bodyLarge + ) + Spacer(modifier = Modifier.padding(5.dp)) + Text( + text = format.format(game.game.modified), + style = MaterialTheme.typography.labelSmall + ) + } + if (game.game.active) { + Badge( + modifier = Modifier.align(TopEnd), + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.primary) { + Text( + text = stringResource(id = R.string.active), + style = MaterialTheme.typography.labelSmall + ) + } + } } } } @@ -326,7 +338,7 @@ fun HistoryListItem( private fun HistoryListPreview() { val tempData = listOf( GameWithScores( - Game(true, "abc", "def", Date(), Date()), listOf(Round(1, 550, 500)) + Game(true, "abcsdf sdaf asdf sdf ", "defsadf asdf sadf ", Date(), Date()), listOf(Round(1, 550, 500)) ), GameWithScores( Game(false, "ADTH", "dogfg", Date(), Date()), listOf(Round(2, 20, 60)) ), GameWithScores( From 4e4653da972decb14c270b4b731a6d8ccaeaa0ba Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 1 Sep 2023 10:54:46 +0200 Subject: [PATCH 13/28] Update to Sdk34 --- app/build.gradle | 40 +++++++++---------- .../me/zobrist/tichucounter/MainActivity.kt | 10 ++--- .../tichucounter/domain/NavExtensions.kt | 15 ------- .../ui/counter/CounterViewModel.kt | 5 ++- .../tichucounter/ui/history/HistoryView.kt | 11 +++-- build.gradle | 2 +- 6 files changed, 34 insertions(+), 49 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d5be58c..9f3597f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,12 +24,12 @@ versionProperties.load(new FileInputStream(versionPropertiesFile)) android { - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "me.zobrist.tichucounter" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode versionProperties["versionCode"].toInteger() versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}" resourceConfigurations += ['de', 'en'] @@ -95,37 +95,37 @@ dependencies { implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.code.gson:gson:2.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' - implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' + implementation 'androidx.navigation:navigation-fragment-ktx:2.7.1' + implementation 'androidx.navigation:navigation-ui-ktx:2.7.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' - implementation 'androidx.fragment:fragment-ktx:1.5.7' - implementation 'androidx.preference:preference-ktx:1.2.0' - implementation 'androidx.recyclerview:recyclerview:1.3.0' + implementation 'androidx.fragment:fragment-ktx:1.6.1' + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' - implementation 'androidx.compose.material:material-icons-extended:1.4.3' + implementation 'androidx.compose.material:material-icons-extended:1.5.0' implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0" implementation 'androidx.activity:activity-compose:1.7.2' - implementation "androidx.compose.ui:ui:1.4.3" - implementation "androidx.compose.ui:ui-tooling-preview:1.4.3" - implementation "androidx.compose.runtime:runtime-livedata:1.4.3" - implementation "androidx.navigation:navigation-compose:2.5.3" + implementation "androidx.compose.ui:ui:1.5.0" + implementation "androidx.compose.ui:ui-tooling-preview:1.5.0" + implementation "androidx.compose.runtime:runtime-livedata:1.5.0" + implementation "androidx.navigation:navigation-compose:2.7.1" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation "com.google.dagger:hilt-android:2.44" - androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.4.3" - debugImplementation "androidx.compose.ui:ui-tooling:1.4.3" - debugImplementation "androidx.compose.ui:ui-test-manifest:1.4.3" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.0" + debugImplementation "androidx.compose.ui:ui-tooling:1.5.0" + debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.0" kapt "com.google.dagger:hilt-compiler:2.44" - implementation "androidx.room:room-runtime:2.5.1" - annotationProcessor "androidx.room:room-compiler:2.5.1" - kapt "androidx.room:room-compiler:2.5.1" - implementation "androidx.room:room-ktx:2.5.1" + implementation "androidx.room:room-runtime:2.5.2" + annotationProcessor "androidx.room:room-compiler:2.5.2" + kapt "androidx.room:room-compiler:2.5.2" + implementation "androidx.room:room-ktx:2.5.2" implementation "androidx.multidex:multidex:2.0.1" - api "androidx.navigation:navigation-fragment-ktx:2.5.3" + api "androidx.navigation:navigation-fragment-ktx:2.7.1" } // Allow references to generated code diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index cb9e40b..2a690bf 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -51,7 +52,6 @@ import me.zobrist.tichucounter.domain.SettingsAdapter import me.zobrist.tichucounter.domain.Theme import me.zobrist.tichucounter.domain.TopBarAction import me.zobrist.tichucounter.domain.TopBarState -import me.zobrist.tichucounter.domain.composable import me.zobrist.tichucounter.domain.navigate import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.MainViewModel @@ -202,7 +202,7 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { startDestination = Route.COUNTER.name, modifier = Modifier.padding(paddings) ) { - composable(Route.COUNTER) { + composable(Route.COUNTER.name) { var expanded by remember { mutableStateOf(false) } @@ -246,7 +246,7 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { Counter(counterViewModel) } - composable(Route.HISTORY) { + composable(Route.HISTORY.name) { topBarState = TopBarState(title = stringResource(R.string.menu_history)) { scope.launch { drawerState.open() } } @@ -255,14 +255,14 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { snackbarHostState ) { navController.navigate(Route.COUNTER) } } - composable(Route.SETTINGS) { + composable(Route.SETTINGS.name) { topBarState = TopBarState(title = stringResource(R.string.menu_settings)) { scope.launch { drawerState.open() } } SettingsView(settingsViewModel) } - composable(Route.ABOUT) { + composable(Route.ABOUT.name) { topBarState = TopBarState(title = stringResource(R.string.menu_about)) { scope.launch { drawerState.open() } } diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt b/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt index a6fdf6d..905cb28 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/NavExtensions.kt @@ -1,12 +1,6 @@ package me.zobrist.tichucounter.domain -import androidx.compose.runtime.Composable -import androidx.navigation.NamedNavArgument -import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController -import androidx.navigation.NavDeepLink -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable fun NavController.navigate(route: Route) { this.navigate(route.name) { @@ -23,12 +17,3 @@ fun NavController.navigate(route: Route) { restoreState = true } } - -fun NavGraphBuilder.composable( - route: Route, - arguments: List = emptyList(), - deepLinks: List = emptyList(), - content: @Composable (NavBackStackEntry) -> Unit -) { - this.composable(route.name, arguments, deepLinks, content) -} diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index 751d186..f943a48 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -1,6 +1,7 @@ package me.zobrist.tichucounter.ui.counter import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.focus.FocusRequester @@ -71,10 +72,10 @@ class CounterViewModel @Inject constructor( override var roundScoreList by mutableStateOf(emptyList()) private set - override var totalScoreA by mutableStateOf(0) + override var totalScoreA by mutableIntStateOf(0) private set - override var totalScoreB by mutableStateOf(0) + override var totalScoreB by mutableIntStateOf(0) private set override var teamNameA by mutableStateOf("") diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index 8f2131b..c3fff06 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -10,12 +10,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items @@ -299,7 +296,7 @@ fun HistoryListItem( Row( Modifier.padding(all = 12.dp) ) { - Box( modifier = modifier.fillMaxSize()) { + Box(modifier = modifier.fillMaxSize()) { Column { Text( text = game.game.nameA + " vs " + game.game.nameB, @@ -321,7 +318,8 @@ fun HistoryListItem( Badge( modifier = Modifier.align(TopEnd), contentColor = MaterialTheme.colorScheme.onPrimary, - containerColor = MaterialTheme.colorScheme.primary) { + containerColor = MaterialTheme.colorScheme.primary + ) { Text( text = stringResource(id = R.string.active), style = MaterialTheme.typography.labelSmall @@ -338,7 +336,8 @@ fun HistoryListItem( private fun HistoryListPreview() { val tempData = listOf( GameWithScores( - Game(true, "abcsdf sdaf asdf sdf ", "defsadf asdf sadf ", Date(), Date()), listOf(Round(1, 550, 500)) + Game(true, "abcsdf sdaf asdf sdf ", "defsadf asdf sadf ", Date(), Date()), + listOf(Round(1, 550, 500)) ), GameWithScores( Game(false, "ADTH", "dogfg", Date(), Date()), listOf(Round(2, 20, 60)) ), GameWithScores( diff --git a/build.gradle b/build.gradle index dc84979..8b75bdf 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,6 @@ allprojects { } } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } \ No newline at end of file From 57b02ce74a10f2efab7ef337cf8fbb808a9ae975 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 1 Sep 2023 10:56:25 +0200 Subject: [PATCH 14/28] Fix keyboard not hidden. --- .../tichucounter/ui/counter/KeyboardView.kt | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt index 32fd620..85880a6 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/KeyboardView.kt @@ -33,6 +33,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -43,6 +44,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalTextInputService import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -290,37 +292,40 @@ fun CenteredTextField( } Box(contentAlignment = Alignment.Center) { - TextField( - value = value, - onValueChange = { }, - placeholder = { - if (!focused) { - Text( - placeholder, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - } - }, - textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), - singleLine = true, - readOnly = true, - modifier = modifier - .fillMaxWidth() - .onFocusChanged { - onFocusStateChanged(it) - } - ) + CompositionLocalProvider(LocalTextInputService provides null) { + TextField( + value = value, + onValueChange = { }, + placeholder = { + if (!focused) { + Text( + placeholder, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + }, + textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), + singleLine = true, + readOnly = true, + modifier = modifier + .fillMaxWidth() + .onFocusChanged { + onFocusStateChanged(it) + } + ) + } + if (focused) { val cursorColor = MaterialTheme.colorScheme.onSurface - val infiniteTransition = rememberInfiniteTransition() + val infiniteTransition = rememberInfiniteTransition(label = "blinkingCursor") val alpha by infiniteTransition.animateFloat( 0f, cursorColor.alpha, animationSpec = infiniteRepeatable( animation = tween(500), repeatMode = RepeatMode.Reverse - ) + ), label = "blinkingCursor" ) Row { From 450c8a154725e68f1c4cdc585a6bfd882663a62f Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Thu, 31 Aug 2023 22:33:54 +0200 Subject: [PATCH 15/28] Should request a review every new game. --- .../me/zobrist/tichucounter/MainActivity.kt | 9 +++ .../tichucounter/domain/ReviewService.kt | 57 +++++++++++++++++++ .../tichucounter/domain/SettingsAdapter.kt | 10 ++++ .../zobrist/tichucounter/ui/MainViewModel.kt | 5 ++ build.gradle | 2 +- 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index 2a690bf..f42db16 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -40,6 +40,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.google.android.play.core.review.ReviewManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -47,6 +48,7 @@ import me.zobrist.tichucounter.domain.DrawerItem import me.zobrist.tichucounter.domain.ISettingsChangeListener import me.zobrist.tichucounter.domain.KeepScreenOn import me.zobrist.tichucounter.domain.Language +import me.zobrist.tichucounter.domain.ReviewService import me.zobrist.tichucounter.domain.Route import me.zobrist.tichucounter.domain.SettingsAdapter import me.zobrist.tichucounter.domain.Theme @@ -73,6 +75,9 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { @Inject lateinit var settingsAdapter: SettingsAdapter + @Inject + lateinit var reviewService: ReviewService + private val counterViewModel: CounterViewModel by viewModels() private val historyViewModel: HistoryViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels() @@ -84,6 +89,10 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { settingsAdapter.registerOnChangeListener(this) + mainViewModel.onNewGame = { + reviewService.request() + } + setContent { AppTheme { val systemUiController = rememberSystemUiController() diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt b/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt new file mode 100644 index 0000000..5653367 --- /dev/null +++ b/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt @@ -0,0 +1,57 @@ +package me.zobrist.tichucounter.domain + +import android.app.Activity +import android.content.Context +import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.android.play.core.review.testing.FakeReviewManager +import dagger.hilt.android.internal.Contexts +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ActivityScoped +import dagger.hilt.android.scopes.FragmentScoped +import dagger.hilt.android.scopes.ViewScoped +import java.time.Duration +import java.time.Period +import java.util.Date +import javax.inject.Inject +import javax.inject.Singleton + +class ReviewService @Inject constructor(@ActivityContext private val appContext: Context) { + + private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext) + + private val THREE_MONTHS : Long = 3 * 30 * 24 * 60 * 60 + + private var lastReviewedDate: Date + get() = Date(sharedPreferences.getLong("lastReviewedDate", 0)) + set(value) { + val editor = sharedPreferences.edit() + editor.putLong("lastReviewedDate", value.time) + editor.apply() + } + + fun request() { + + val diff = Date().time - lastReviewedDate.time + + if(diff > 0) + { + lastReviewedDate = Date() + + val manager = ReviewManagerFactory.create(appContext) + + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + // We got the ReviewInfo object + val reviewInfo = task.result + manager.launchReviewFlow(appContext as Activity, reviewInfo) + + } else { + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt index 452e678..6f7e074 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.core.os.LocaleListCompat import androidx.preference.PreferenceManager import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.Date import javax.inject.Inject import javax.inject.Singleton @@ -36,6 +37,9 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex var keepScreenOn: KeepScreenOn private set + var reviewDialogShownDate: Date + get() = Date(sharedPreferences.getLong("reviewDialogShownDate", 0)) + set(value) = updatePreference("reviewDialogShownDate", value.time) init { language = try { @@ -95,6 +99,12 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex editor.apply() } + private fun updatePreference(name: String?, value: Long) { + val editor = sharedPreferences.edit() + editor.putLong(name, value) + editor.apply() + } + private fun notifyListeners(language: Language) { listenerList.forEach { it.onLanguageChanged(language) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt index 5905bf0..022f07b 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt @@ -7,8 +7,10 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.launch import me.zobrist.tichucounter.data.entity.Round +import me.zobrist.tichucounter.domain.ReviewService import me.zobrist.tichucounter.repository.GameRepository import javax.inject.Inject @@ -21,6 +23,8 @@ class MainViewModel @Inject constructor( private var redoRounds = mutableStateListOf() private var expectedRoundCount = 0 + var onNewGame: (() -> Unit)? = null + var isUndoActionActive by mutableStateOf(false) val isRedoActionActive: Boolean @@ -75,5 +79,6 @@ class MainViewModel @Inject constructor( redoRounds.clear() gameRepository.newGame() } + onNewGame?.let { it() } } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8b75bdf..75ce81e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From dbef6e047a1de824143abbd6a5d8a319b70fc8eb Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 1 Sep 2023 13:00:41 +0200 Subject: [PATCH 16/28] Request review only every third game after three months. --- .../tichucounter/domain/ReviewService.kt | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt b/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt index 5653367..63a1507 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt @@ -22,9 +22,17 @@ class ReviewService @Inject constructor(@ActivityContext private val appContext: private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext) - private val THREE_MONTHS : Long = 3 * 30 * 24 * 60 * 60 + private val THREE_MONTHS : Long = 7776000000 - private var lastReviewedDate: Date + private var requestCalled: Int + get() = sharedPreferences.getInt("requestCalled", 0) + set(value) { + val editor = sharedPreferences.edit() + editor.putInt("requestCalled", value) + editor.apply() + } + + private var nextReviewedDate: Date get() = Date(sharedPreferences.getLong("lastReviewedDate", 0)) set(value) { val editor = sharedPreferences.edit() @@ -33,23 +41,26 @@ class ReviewService @Inject constructor(@ActivityContext private val appContext: } fun request() { + requestCalled += 1 - val diff = Date().time - lastReviewedDate.time - - if(diff > 0) + if(requestCalled >= 3) { - lastReviewedDate = Date() + if(nextReviewedDate.time < System.currentTimeMillis()) + { + requestCalled = 0 + nextReviewedDate = Date(System.currentTimeMillis() + THREE_MONTHS) - val manager = ReviewManagerFactory.create(appContext) + val manager = ReviewManagerFactory.create(appContext) - val request = manager.requestReviewFlow() - request.addOnCompleteListener { task -> - if (task.isSuccessful) { - // We got the ReviewInfo object - val reviewInfo = task.result - manager.launchReviewFlow(appContext as Activity, reviewInfo) + val request = manager.requestReviewFlow() + request.addOnCompleteListener { task -> + if (task.isSuccessful) { + // We got the ReviewInfo object + val reviewInfo = task.result + manager.launchReviewFlow(appContext as Activity, reviewInfo) - } else { + } else { + } } } } From c97d98704bcadbb43f208e92f828ef4f03ac6c7a Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 26 Aug 2023 13:00:30 +0200 Subject: [PATCH 17/28] Update view with victory points setting. Add victory points to settings adapter. --- .../me/zobrist/tichucounter/MainActivity.kt | 12 +-- .../tichucounter/domain/SettingsAdapter.kt | 56 ++++++++--- .../ui/composables/DropDownMenu.kt | 16 ++-- .../tichucounter/ui/counter/CounterView.kt | 1 + .../ui/counter/CounterViewModel.kt | 17 +++- .../tichucounter/ui/settings/SettingsView.kt | 92 +++++++++++-------- .../ui/settings/SettingsViewModel.kt | 7 ++ app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values/strings.xml | 3 + 9 files changed, 146 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index f42db16..1f62ef8 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -45,7 +45,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import me.zobrist.tichucounter.domain.DrawerItem -import me.zobrist.tichucounter.domain.ISettingsChangeListener +import me.zobrist.tichucounter.domain.ISystemSettingsChangeListener import me.zobrist.tichucounter.domain.KeepScreenOn import me.zobrist.tichucounter.domain.Language import me.zobrist.tichucounter.domain.ReviewService @@ -70,7 +70,7 @@ import me.zobrist.tichucounter.ui.settings.SettingsViewModel import javax.inject.Inject @AndroidEntryPoint -class MainActivity : AppCompatActivity(), ISettingsChangeListener { +class MainActivity : AppCompatActivity(), ISystemSettingsChangeListener { @Inject lateinit var settingsAdapter: SettingsAdapter @@ -192,7 +192,6 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { var topBarState by remember { mutableStateOf(TopBarState()) } var snackbarHostState by remember { mutableStateOf(SnackbarHostState()) } - val scope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) }, @@ -211,7 +210,7 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { startDestination = Route.COUNTER.name, modifier = Modifier.padding(paddings) ) { - composable(Route.COUNTER.name) { + this.composable(Route.COUNTER.name.toString()) { var expanded by remember { mutableStateOf(false) } @@ -231,15 +230,16 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener { mainViewModel.activeGameHasRounds, { expanded = true } ) { + val newGameTranslated = stringResource(R.string.newGame) DropDownMenu( - mapOf("new" to R.string.newGame), + listOf(newGameTranslated), "", expanded, ) { expanded = false it?.let { when (it) { - "new" -> mainViewModel.newGame() + newGameTranslated -> mainViewModel.newGame() } } } diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt index 6f7e074..52862d0 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt @@ -17,12 +17,19 @@ enum class Language(val value: LocaleListCompat) { enum class KeepScreenOn(val value: Boolean) { ON(true), OFF(false) } -interface ISettingsChangeListener { +typealias VictoryPoints = Int + +interface ISettingsChangeListener +interface ISystemSettingsChangeListener : ISettingsChangeListener { fun onLanguageChanged(language: Language) fun onThemeChanged(theme: Theme) fun onScreenOnChanged(keepOn: KeepScreenOn) } +interface IGameSettingsChangeListener : ISettingsChangeListener { + fun onVictoryPointsChanged(victoryPoints: Int) +} + @Singleton class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) { @@ -37,9 +44,9 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex var keepScreenOn: KeepScreenOn private set - var reviewDialogShownDate: Date - get() = Date(sharedPreferences.getLong("reviewDialogShownDate", 0)) - set(value) = updatePreference("reviewDialogShownDate", value.time) + + var victoryPoints: Int + private set init { language = try { @@ -59,14 +66,22 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex } catch (_: java.lang.Exception) { KeepScreenOn.OFF } + + victoryPoints = sharedPreferences.getInt(VictoryPoints::class.simpleName, 1000) } fun registerOnChangeListener(listener: ISettingsChangeListener) { listenerList.add(listener) - listener.onThemeChanged(theme) - listener.onLanguageChanged(language) - listener.onScreenOnChanged(keepScreenOn) + if (listener is ISystemSettingsChangeListener) { + listener.onThemeChanged(theme) + listener.onLanguageChanged(language) + listener.onScreenOnChanged(keepScreenOn) + } + + if (listener is IGameSettingsChangeListener) { + listener.onVictoryPointsChanged(victoryPoints) + } } fun unregisterOnChangeListener(listener: ISettingsChangeListener?) { @@ -93,6 +108,12 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex notifyListeners(setting) } + fun setVictoryPoints(setting: Int) { + this.victoryPoints = setting + updatePreference(VictoryPoints::class.simpleName, setting) + notifyListeners(setting) + } + private fun updatePreference(name: String?, value: String) { val editor = sharedPreferences.edit() editor.putString(name, value) @@ -105,22 +126,33 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex editor.apply() } + private fun updatePreference(name: String?, value: Int) { + val editor = sharedPreferences.edit() + editor.putInt(name, value) + editor.apply() + } + private fun notifyListeners(language: Language) { - listenerList.forEach { + listenerList.filterIsInstance().forEach { it.onLanguageChanged(language) } } private fun notifyListeners(theme: Theme) { - listenerList.forEach { + listenerList.filterIsInstance().forEach { it.onThemeChanged(theme) } } - private fun notifyListeners(keepScreenOn: KeepScreenOn) { - listenerList.forEach { - it.onScreenOnChanged(keepScreenOn) + private fun notifyListeners(victoryPoints: VictoryPoints) { + listenerList.filterIsInstance().forEach { + it.onVictoryPointsChanged(victoryPoints) } } + private fun notifyListeners(keepScreenOn: KeepScreenOn) { + listenerList.filterIsInstance().forEach { + it.onScreenOnChanged(keepScreenOn) + } + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt b/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt index 12a9700..6042af4 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/composables/DropDownMenu.kt @@ -7,25 +7,29 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource @Composable -fun DropDownMenu(map: Map, selected: T, expanded: Boolean, onSelected: (T?) -> Unit) { +fun DropDownMenu( + list: Collection, + selected: T, + expanded: Boolean, + onSelected: (T?) -> Unit +) { DropdownMenu( expanded = expanded, onDismissRequest = { onSelected(null) } ) { - map.forEach { + list.forEach { DropdownMenuItem( onClick = { - onSelected(it.key) + onSelected(it) }, trailingIcon = { - if (it.key == selected) { + if (it == selected) { Icon(Icons.Outlined.Check, null) } }, - text = { Text(stringResource(it.value)) }, + text = { Text(it.toString()) }, ) } } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt index a338d72..e9d7da3 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt @@ -123,6 +123,7 @@ internal class PreviewViewModel : ICounterViewModel { listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long") override val teamNameSuggestionsB: List = listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long") + override val victoryPoints: Int = 1000 override fun focusLastInput() { } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index f943a48..4439cf5 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.zobrist.tichucounter.data.entity.Round +import me.zobrist.tichucounter.domain.IGameSettingsChangeListener import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.digitCount import me.zobrist.tichucounter.domain.getTotalPoints @@ -58,6 +59,7 @@ interface ICounterViewModel : IKeyBoardViewModel { val teamNameB: String val teamNameSuggestionsA: List val teamNameSuggestionsB: List + val victoryPoints: Int fun updateNameA(value: String) fun updateNameB(value: String) @@ -67,7 +69,7 @@ interface ICounterViewModel : IKeyBoardViewModel { class CounterViewModel @Inject constructor( private val gameRepository: GameRepository ) : - ViewModel(), ICounterViewModel { + ViewModel(), ICounterViewModel, IGameSettingsChangeListener { override var roundScoreList by mutableStateOf(emptyList()) private set @@ -114,6 +116,9 @@ class CounterViewModel @Inject constructor( override var teamNameSuggestionsB by mutableStateOf(listOf()) private set + override var victoryPoints by mutableStateOf(0) + private set + override var activeValue: String get() { return if (isBFocused) { @@ -171,6 +176,12 @@ class CounterViewModel @Inject constructor( buildTeamNameSuggestions() + if (totalScoreA >= victoryPoints || totalScoreB >= victoryPoints) { + if (totalScoreA == totalScoreB) { + + } + } + } } } @@ -330,6 +341,10 @@ class CounterViewModel @Inject constructor( } } + override fun onVictoryPointsChanged(victoryPoints: Int) { + this.victoryPoints = victoryPoints + } + private fun deleteLastDigitActive() { if (activeValue != "") { activeValue = activeValue.dropLast(1) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt index bcdddf4..83ffacb 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsView.kt @@ -44,6 +44,7 @@ val themeMap = mapOf( Theme.LIGHT to R.string.light ) +val victoryPointsList = listOf(500, 1000, 1500, 2000) @Composable fun SettingsView(viewModel: SettingsViewModel) { @@ -51,9 +52,11 @@ fun SettingsView(viewModel: SettingsViewModel) { viewModel.screenOn.value, viewModel.language, viewModel.theme, + viewModel.victoryPoints, { viewModel.updateScreenOn(it) }, { viewModel.updateLanguage(it) }, - { viewModel.updateTheme(it) }) + { viewModel.updateTheme(it) }, + { viewModel.updateVictoryPoints(it) }) } @Composable @@ -61,11 +64,22 @@ fun SettingsView( valueScreenOn: Boolean = true, valueLanguage: Language = Language.ENGLISH, valueTheme: Theme = Theme.DARK, + valueVictoryPoints: Int = 1000, updateScreenOn: (KeepScreenOn) -> Unit = {}, updateLanguage: (Language) -> Unit = {}, - updateTheme: (Theme) -> Unit = {} + updateTheme: (Theme) -> Unit = {}, + updateVictoryPoints: (Int) -> Unit = {} ) { - Column { + Column( + Modifier + .padding(20.dp) + ) { + + Text( + text = stringResource(R.string.display), + style = MaterialTheme.typography.headlineMedium + ) + BooleanSetting( stringResource(R.string.keep_screen_on), valueScreenOn @@ -82,6 +96,18 @@ fun SettingsView( themeMap, valueTheme, ) { updateTheme(it) } + + Text( + text = stringResource(R.string.game), + style = MaterialTheme.typography.headlineMedium + ) + + + ListSetting( + stringResource(R.string.victory_points), + victoryPointsList, + valueVictoryPoints + ) { updateVictoryPoints(it) } } } @@ -90,7 +116,7 @@ fun BooleanSetting(name: String, value: Boolean, updateValue: (Boolean) -> Unit) Row( Modifier - .padding(20.dp) + .padding(bottom = 15.dp, top = 5.dp) .fillMaxWidth() ) { Column(Modifier.weight(5f)) { @@ -119,21 +145,32 @@ fun BooleanSetting(name: String, value: Boolean, updateValue: (Boolean) -> Unit) @Composable fun StringSetting(name: String, map: Map, selected: T, onSelected: (T) -> Unit) { + val translated = map.map { it.key to stringResource(it.value) }.toMap() + val getValue = map.map { stringResource(it.value) to it.key }.toMap() + + ListSetting( + name, + translated.values, + translated[selected] + ) { getValue[it]?.let { it1 -> onSelected(it1) } } +} + +@Composable +fun ListSetting(name: String, list: Collection, selected: T, onSelected: (T) -> Unit) { + var expanded by remember { mutableStateOf(false) } Row( Modifier .fillMaxWidth() - .padding(20.dp) + .padding(bottom = 15.dp, top = 5.dp) .clickable { expanded = true }) { Column(Modifier.weight(5f)) { Text(name, style = MaterialTheme.typography.bodyLarge, overflow = TextOverflow.Ellipsis) - map[selected]?.let { - Text( - stringResource(it), - style = MaterialTheme.typography.labelLarge - ) - } + Text( + selected.toString(), + style = MaterialTheme.typography.labelLarge + ) } Column(Modifier.weight(1f)) { @@ -142,15 +179,15 @@ fun StringSetting(name: String, map: Map, selected: T, onSelected: ( contentDescription = null, modifier = Modifier.align(End) ) - } - DropDownMenu( - map, - selected, - expanded, - ) { - expanded = false - it?.let { onSelected(it) } + DropDownMenu( + list, + selected, + expanded, + ) { + expanded = false + it?.let { onSelected(it) } + } } } } @@ -167,20 +204,3 @@ fun SettingsViewPreview() { } } } - -@Preview(name = "Light Mode") -@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) -@Composable -fun StringSettingPreview() { - - AppTheme { - Surface { - DropDownMenu( - themeMap, - Theme.LIGHT, - true, - ) {} - } - } -} - diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt index b37e1bd..b3657b3 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt @@ -24,6 +24,9 @@ class SettingsViewModel @Inject constructor(private val settings: SettingsAdapte var screenOn by mutableStateOf(settings.keepScreenOn) private set + var victoryPoints by mutableStateOf(settings.victoryPoints) + private set + fun updateLanguage(language: Language) { settings.setLanguage(language) this.language = settings.language @@ -39,4 +42,8 @@ class SettingsViewModel @Inject constructor(private val settings: SettingsAdapte screenOn = settings.keepScreenOn } + fun updateVictoryPoints(value: Int) { + settings.setVictoryPoints(value) + victoryPoints = settings.victoryPoints + } } \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d2138cd..82c3530 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -29,5 +29,7 @@ RÜCKGÄNGIG Spiel aktiviert. WEITERSPIELEN - + Anzeige + Spiel + Siegespunkte \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d18461..ff61cf2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,4 +33,7 @@ UNDO Game activated. CONTINUE PLAYING + Display + Game + Victory points \ No newline at end of file From 2599e3320a28f5813a94e3f2fde755e8b7ed248a Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 26 Aug 2023 15:20:43 +0200 Subject: [PATCH 18/28] Add alerts for winning and draw. --- .../me/zobrist/tichucounter/MainActivity.kt | 3 +- .../tichucounter/domain/ReviewService.kt | 18 +---- .../tichucounter/domain/SettingsAdapter.kt | 1 - .../zobrist/tichucounter/ui/MainViewModel.kt | 2 - .../tichucounter/ui/counter/CounterView.kt | 73 ++++++++++++++++++- .../ui/counter/CounterViewModel.kt | 53 ++++++++++---- app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values/strings.xml | 8 +- 8 files changed, 127 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index 1f62ef8..ca5b423 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -40,7 +40,6 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.google.android.play.core.review.ReviewManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -230,7 +229,7 @@ class MainActivity : AppCompatActivity(), ISystemSettingsChangeListener { mainViewModel.activeGameHasRounds, { expanded = true } ) { - val newGameTranslated = stringResource(R.string.newGame) + val newGameTranslated = stringResource(R.string.new_game) DropDownMenu( listOf(newGameTranslated), "", diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt b/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt index 63a1507..bc2c806 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/ReviewService.kt @@ -2,27 +2,17 @@ package me.zobrist.tichucounter.domain import android.app.Activity import android.content.Context -import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.google.android.play.core.review.ReviewManagerFactory -import com.google.android.play.core.review.testing.FakeReviewManager -import dagger.hilt.android.internal.Contexts import dagger.hilt.android.qualifiers.ActivityContext -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.android.scopes.ActivityScoped -import dagger.hilt.android.scopes.FragmentScoped -import dagger.hilt.android.scopes.ViewScoped -import java.time.Duration -import java.time.Period import java.util.Date import javax.inject.Inject -import javax.inject.Singleton class ReviewService @Inject constructor(@ActivityContext private val appContext: Context) { private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext) - private val THREE_MONTHS : Long = 7776000000 + private val THREE_MONTHS: Long = 7776000000 private var requestCalled: Int get() = sharedPreferences.getInt("requestCalled", 0) @@ -43,10 +33,8 @@ class ReviewService @Inject constructor(@ActivityContext private val appContext: fun request() { requestCalled += 1 - if(requestCalled >= 3) - { - if(nextReviewedDate.time < System.currentTimeMillis()) - { + if (requestCalled >= 3) { + if (nextReviewedDate.time < System.currentTimeMillis()) { requestCalled = 0 nextReviewedDate = Date(System.currentTimeMillis() + THREE_MONTHS) diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt index 52862d0..e011f75 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.core.os.LocaleListCompat import androidx.preference.PreferenceManager import dagger.hilt.android.qualifiers.ApplicationContext -import java.util.Date import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt index 022f07b..db9d2b4 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt @@ -7,10 +7,8 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.launch import me.zobrist.tichucounter.data.entity.Round -import me.zobrist.tichucounter.domain.ReviewService import me.zobrist.tichucounter.repository.GameRepository import javax.inject.Inject diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt index e9d7da3..1c6c68d 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt @@ -3,7 +3,13 @@ package me.zobrist.tichucounter.ui.counter import android.content.res.Configuration import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.EmojiEvents +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -12,7 +18,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import me.zobrist.tichucounter.R import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.ui.AppTheme @@ -23,6 +31,18 @@ fun Counter(viewModel: ICounterViewModel = PreviewViewModel()) { var orientation by remember { mutableStateOf(Configuration.ORIENTATION_PORTRAIT) } orientation = LocalConfiguration.current.orientation + if (viewModel.showVictoryDialog) { + GameVictoryDialog( + viewModel.totalScoreA, + viewModel.totalScoreB, + viewModel.teamNameA, + viewModel.teamNameB, + { viewModel.victoryDialogExecuted(false) }) + { + viewModel.victoryDialogExecuted(true) + } + } + Surface { if (orientation == Configuration.ORIENTATION_LANDSCAPE) { Landscape(viewModel) @@ -102,6 +122,54 @@ fun CounterViewPreview() { } } +@Preview() +@Composable +fun GameVictoryDialog( + pointsA: Int = 2000, + pointsB: Int = 50, + nameA: String = "nameA", + nameB: String = "nameB", + onDismiss: () -> Unit = {}, + onNewGame: () -> Unit = {}, +) { + + val winner = if (pointsA > pointsB) { + nameA + } else { + nameB + } + + val message = if (pointsA == pointsB) { + stringResource(R.string.draw_message, winner, 100) + } else { + stringResource(R.string.victory_message) + } + + val title = if (pointsA == pointsB) { + stringResource(R.string.draw_title) + } else { + stringResource(R.string.victory_title, winner) + } + + AlertDialog( + onDismissRequest = { onDismiss() }, + dismissButton = { + TextButton({ onDismiss() }) { + Text(stringResource(R.string.continue_play)) + } + }, + confirmButton = { + TextButton({ onNewGame() }) { + Text(stringResource(R.string.new_game)) + } + }, + icon = { Icon(Icons.Outlined.EmojiEvents, null) }, + title = { Text(title) }, + text = { Text(message) } + + ) +} + internal class PreviewViewModel : ICounterViewModel { override var roundScoreList: List = listOf(Round(1, 10, 90), Round(1, 50, 50), Round(1, 70, 30)) @@ -123,7 +191,7 @@ internal class PreviewViewModel : ICounterViewModel { listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long") override val teamNameSuggestionsB: List = listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long") - override val victoryPoints: Int = 1000 + override var showVictoryDialog: Boolean = false override fun focusLastInput() { } @@ -156,6 +224,9 @@ internal class PreviewViewModel : ICounterViewModel { override fun updateNameB(value: String) { } + override fun victoryDialogExecuted(result: Boolean) { + } + override fun updateFocusStateA(state: Boolean) { } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index 4439cf5..e56330f 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -11,8 +11,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.domain.IGameSettingsChangeListener +import me.zobrist.tichucounter.domain.SettingsAdapter import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.digitCount import me.zobrist.tichucounter.domain.getTotalPoints @@ -59,15 +61,17 @@ interface ICounterViewModel : IKeyBoardViewModel { val teamNameB: String val teamNameSuggestionsA: List val teamNameSuggestionsB: List - val victoryPoints: Int + val showVictoryDialog: Boolean fun updateNameA(value: String) fun updateNameB(value: String) + fun victoryDialogExecuted(result: Boolean) } @HiltViewModel class CounterViewModel @Inject constructor( - private val gameRepository: GameRepository + private val gameRepository: GameRepository, + private val settings: SettingsAdapter ) : ViewModel(), ICounterViewModel, IGameSettingsChangeListener { @@ -115,8 +119,7 @@ class CounterViewModel @Inject constructor( override var teamNameSuggestionsB by mutableStateOf(listOf()) private set - - override var victoryPoints by mutableStateOf(0) + override var showVictoryDialog by mutableStateOf(false) private set override var activeValue: String @@ -160,11 +163,14 @@ class CounterViewModel @Inject constructor( private var distinctTeamNames = listOf() + private var victoryDialogShown = false + + private var lastGame: Game? = null + init { viewModelScope.launch { gameRepository.getActiveGameFlow().collect { if (it != null) { - val score = it.getTotalPoints() roundScoreList = it.rounds @@ -176,12 +182,16 @@ class CounterViewModel @Inject constructor( buildTeamNameSuggestions() - if (totalScoreA >= victoryPoints || totalScoreB >= victoryPoints) { - if (totalScoreA == totalScoreB) { - - } + if (it.game.uid != lastGame?.uid) { + victoryDialogShown = false + lastGame = it.game } + if (!victoryDialogShown) { + if (totalScoreA >= settings.victoryPoints || totalScoreB >= settings.victoryPoints) { + showVictoryDialog = true + } + } } } } @@ -193,6 +203,12 @@ class CounterViewModel @Inject constructor( buildTeamNameSuggestions() } } + + settings.registerOnChangeListener(this) + } + + override fun onCleared() { + settings.unregisterOnChangeListener(this) } override fun focusLastInput() { @@ -301,6 +317,17 @@ class CounterViewModel @Inject constructor( } } + override fun victoryDialogExecuted(result: Boolean) { + showVictoryDialog = false + victoryDialogShown = true + + if (result) { + viewModelScope.launch { + gameRepository.newGame() + } + } + } + override fun updateFocusStateA(state: Boolean) { isAFocused = state if (state) { @@ -341,10 +368,6 @@ class CounterViewModel @Inject constructor( } } - override fun onVictoryPointsChanged(victoryPoints: Int) { - this.victoryPoints = victoryPoints - } - private fun deleteLastDigitActive() { if (activeValue != "") { activeValue = activeValue.dropLast(1) @@ -378,4 +401,8 @@ class CounterViewModel @Inject constructor( return filtered.sorted().sortedBy { it.length }.take(10) } + + override fun onVictoryPointsChanged(victoryPoints: Int) { + victoryDialogShown = false + } } \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 82c3530..be1fb4e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -12,7 +12,7 @@ Einstellungen Ein Aus - Neues Spiel + Neues Spiel Verlauf löschen Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden. Abbrechen @@ -32,4 +32,8 @@ Anzeige Spiel Siegespunkte + %1$s hat gewonnen + Sieht aus, als ob ihr ein neues Spiel starten solltet, um das endgültig zu klären. + Unentschieden + Herzliche Gratulation! Wie wäre es mit einer Revanche? \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff61cf2..f477b30 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ Settings On Off - New Game + New Game Delete history You really want to delete the history? This action can\'t be undone. Cancel @@ -28,7 +28,7 @@ About Contact us Play Store - Continue + Continue game Game deleted. UNDO Game activated. @@ -36,4 +36,8 @@ Display Game Victory points + %1$s won the game + Looks like you should start a new game to settle this for good. + Draw + Congratulations! How about a rematch? \ No newline at end of file From 6a96749501f241872ab85797790a51097360957a Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 1 Sep 2023 18:31:21 +0200 Subject: [PATCH 19/28] Increase version. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 9f3597f..a636bc3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ def keystoreProperties = new Properties() def versionProperties = new Properties() def versionMajor = 2 -def versionMinor = 2 +def versionMinor = 3 // Load your keystore.properties file into the keystoreProperties object. keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) From 27cb2f670b80f56904e0e0da64861ca5ceb341d3 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 8 Sep 2023 18:20:56 +0200 Subject: [PATCH 20/28] Settings as state flow. --- .../me/zobrist/tichucounter/MainActivity.kt | 36 ++++-- .../tichucounter/domain/SettingsAdapter.kt | 113 +++++------------- .../ui/counter/CounterViewModel.kt | 19 +-- .../ui/settings/SettingsViewModel.kt | 25 ++-- 4 files changed, 73 insertions(+), 120 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index ca5b423..751a6ec 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -44,7 +45,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import me.zobrist.tichucounter.domain.DrawerItem -import me.zobrist.tichucounter.domain.ISystemSettingsChangeListener import me.zobrist.tichucounter.domain.KeepScreenOn import me.zobrist.tichucounter.domain.Language import me.zobrist.tichucounter.domain.ReviewService @@ -69,7 +69,7 @@ import me.zobrist.tichucounter.ui.settings.SettingsViewModel import javax.inject.Inject @AndroidEntryPoint -class MainActivity : AppCompatActivity(), ISystemSettingsChangeListener { +class MainActivity : AppCompatActivity() { @Inject lateinit var settingsAdapter: SettingsAdapter @@ -86,7 +86,26 @@ class MainActivity : AppCompatActivity(), ISystemSettingsChangeListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - settingsAdapter.registerOnChangeListener(this) + changeTheme(settingsAdapter.theme.value) + setKeepScreenOn(settingsAdapter.keepScreenOn.value) + changeLanguage(settingsAdapter.language.value) + + + lifecycleScope.launch { + settingsAdapter.theme.collect { + changeTheme(it) + } + } + lifecycleScope.launch { + settingsAdapter.keepScreenOn.collect { + setKeepScreenOn(it) + } + } + lifecycleScope.launch { + settingsAdapter.language.collect { + changeLanguage(it) + } + } mainViewModel.onNewGame = { reviewService.request() @@ -101,16 +120,11 @@ class MainActivity : AppCompatActivity(), ISystemSettingsChangeListener { } } - override fun onDestroy() { - super.onDestroy() - settingsAdapter.unregisterOnChangeListener(this) - } - - override fun onLanguageChanged(language: Language) { + private fun changeLanguage(language: Language) { AppCompatDelegate.setApplicationLocales(language.value) } - override fun onThemeChanged(theme: Theme) { + private fun changeTheme(theme: Theme) { val themeValue = when (theme) { Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES @@ -119,7 +133,7 @@ class MainActivity : AppCompatActivity(), ISystemSettingsChangeListener { AppCompatDelegate.setDefaultNightMode(themeValue) } - override fun onScreenOnChanged(keepOn: KeepScreenOn) { + private fun setKeepScreenOn(keepOn: KeepScreenOn) { if (keepOn.value) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } else { diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt index e011f75..70acbbd 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt @@ -4,6 +4,10 @@ import android.content.Context import androidx.core.os.LocaleListCompat import androidx.preference.PreferenceManager import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @@ -18,99 +22,64 @@ enum class KeepScreenOn(val value: Boolean) { ON(true), OFF(false) } typealias VictoryPoints = Int -interface ISettingsChangeListener -interface ISystemSettingsChangeListener : ISettingsChangeListener { - fun onLanguageChanged(language: Language) - fun onThemeChanged(theme: Theme) - fun onScreenOnChanged(keepOn: KeepScreenOn) -} - -interface IGameSettingsChangeListener : ISettingsChangeListener { - fun onVictoryPointsChanged(victoryPoints: Int) -} - @Singleton class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) { private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - private var listenerList = mutableListOf() - var language: Language - private set + val language = MutableStateFlow(Language.DEFAULT) - var theme: Theme - private set + val theme = MutableStateFlow(Theme.DEFAULT) - var keepScreenOn: KeepScreenOn - private set + val keepScreenOn = MutableStateFlow(KeepScreenOn.OFF) - var victoryPoints: Int - private set + val victoryPoints = MutableStateFlow(0) init { - language = try { + language.value = try { enumValueOf(sharedPreferences.getString(Language::class.simpleName, null)!!) } catch (_: NullPointerException) { Language.DEFAULT } - theme = try { + theme.value = try { enumValueOf(sharedPreferences.getString(Theme::class.simpleName, null)!!) } catch (_: java.lang.Exception) { Theme.DEFAULT } - keepScreenOn = try { + keepScreenOn.value = try { enumValueOf(sharedPreferences.getString(KeepScreenOn::class.simpleName, null)!!) } catch (_: java.lang.Exception) { KeepScreenOn.OFF } - victoryPoints = sharedPreferences.getInt(VictoryPoints::class.simpleName, 1000) - } + victoryPoints.value = sharedPreferences.getInt(VictoryPoints::class.simpleName, 1000) - fun registerOnChangeListener(listener: ISettingsChangeListener) { - listenerList.add(listener) - if (listener is ISystemSettingsChangeListener) { - listener.onThemeChanged(theme) - listener.onLanguageChanged(language) - listener.onScreenOnChanged(keepScreenOn) + CoroutineScope(Dispatchers.IO).launch { + language.collect { + updatePreference(Language::class.simpleName, it.name) + } } - if (listener is IGameSettingsChangeListener) { - listener.onVictoryPointsChanged(victoryPoints) + CoroutineScope(Dispatchers.IO).launch { + theme.collect { + updatePreference(Theme::class.simpleName, it.name) + } } - } - fun unregisterOnChangeListener(listener: ISettingsChangeListener?) { - if (listener != null) { - listenerList.remove(listener) + CoroutineScope(Dispatchers.IO).launch { + keepScreenOn.collect { + updatePreference(KeepScreenOn::class.simpleName, it.name) + } } - } - fun setLanguage(language: Language) { - this.language = language - updatePreference(Language::class.simpleName, language.name) - notifyListeners(language) - } - - fun setTheme(theme: Theme) { - this.theme = theme - updatePreference(Theme::class.simpleName, theme.name) - notifyListeners(theme) - } - - fun setKeepScreenOn(setting: KeepScreenOn) { - this.keepScreenOn = setting - updatePreference(KeepScreenOn::class.simpleName, setting.name) - notifyListeners(setting) - } - - fun setVictoryPoints(setting: Int) { - this.victoryPoints = setting - updatePreference(VictoryPoints::class.simpleName, setting) - notifyListeners(setting) + CoroutineScope(Dispatchers.IO).launch { + victoryPoints.collect { + updatePreference(VictoryPoints::class.simpleName, it) + } + } } private fun updatePreference(name: String?, value: String) { @@ -130,28 +99,4 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex editor.putInt(name, value) editor.apply() } - - private fun notifyListeners(language: Language) { - listenerList.filterIsInstance().forEach { - it.onLanguageChanged(language) - } - } - - private fun notifyListeners(theme: Theme) { - listenerList.filterIsInstance().forEach { - it.onThemeChanged(theme) - } - } - - private fun notifyListeners(victoryPoints: VictoryPoints) { - listenerList.filterIsInstance().forEach { - it.onVictoryPointsChanged(victoryPoints) - } - } - - private fun notifyListeners(keepScreenOn: KeepScreenOn) { - listenerList.filterIsInstance().forEach { - it.onScreenOnChanged(keepScreenOn) - } - } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index e56330f..a18f8a5 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round -import me.zobrist.tichucounter.domain.IGameSettingsChangeListener import me.zobrist.tichucounter.domain.SettingsAdapter import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.digitCount @@ -73,7 +72,7 @@ class CounterViewModel @Inject constructor( private val gameRepository: GameRepository, private val settings: SettingsAdapter ) : - ViewModel(), ICounterViewModel, IGameSettingsChangeListener { + ViewModel(), ICounterViewModel { override var roundScoreList by mutableStateOf(emptyList()) private set @@ -188,7 +187,7 @@ class CounterViewModel @Inject constructor( } if (!victoryDialogShown) { - if (totalScoreA >= settings.victoryPoints || totalScoreB >= settings.victoryPoints) { + if (totalScoreA >= settings.victoryPoints.value || totalScoreB >= settings.victoryPoints.value) { showVictoryDialog = true } } @@ -202,13 +201,11 @@ class CounterViewModel @Inject constructor( buildTeamNameSuggestions() } + + settings.victoryPoints.collect { + victoryDialogShown = false + } } - - settings.registerOnChangeListener(this) - } - - override fun onCleared() { - settings.unregisterOnChangeListener(this) } override fun focusLastInput() { @@ -401,8 +398,4 @@ class CounterViewModel @Inject constructor( return filtered.sorted().sortedBy { it.length }.take(10) } - - override fun onVictoryPointsChanged(victoryPoints: Int) { - victoryDialogShown = false - } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt index b3657b3..cd41a25 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/settings/SettingsViewModel.kt @@ -1,6 +1,7 @@ package me.zobrist.tichucounter.ui.settings import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel @@ -15,35 +16,35 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor(private val settings: SettingsAdapter) : ViewModel() { - var language by mutableStateOf(settings.language) + var language by mutableStateOf(settings.language.value) private set - var theme by mutableStateOf(settings.theme) + var theme by mutableStateOf(settings.theme.value) private set - var screenOn by mutableStateOf(settings.keepScreenOn) + var screenOn by mutableStateOf(settings.keepScreenOn.value) private set - var victoryPoints by mutableStateOf(settings.victoryPoints) + var victoryPoints by mutableIntStateOf(settings.victoryPoints.value) private set fun updateLanguage(language: Language) { - settings.setLanguage(language) - this.language = settings.language + settings.language.value = language + this.language = language } fun updateTheme(theme: Theme) { - settings.setTheme(theme) - this.theme = settings.theme + settings.theme.value = theme + this.theme = theme } fun updateScreenOn(value: KeepScreenOn) { - settings.setKeepScreenOn(value) - screenOn = settings.keepScreenOn + settings.keepScreenOn.value = value + screenOn = value } fun updateVictoryPoints(value: Int) { - settings.setVictoryPoints(value) - victoryPoints = settings.victoryPoints + settings.victoryPoints.value = value + victoryPoints = value } } \ No newline at end of file From 81a0d680e9af4f1d7a3452e476ad9df3d0ecb45e Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Fri, 8 Sep 2023 18:40:52 +0200 Subject: [PATCH 21/28] Flow on new game. Now review is always requested regardless of where the new game was started. --- .../me/zobrist/tichucounter/MainActivity.kt | 20 ++++++++++++--- .../tichucounter/repository/GameRepository.kt | 10 ++++++++ .../zobrist/tichucounter/ui/MainViewModel.kt | 25 +++++++++++-------- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index 751a6ec..6d80823 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -43,7 +43,9 @@ import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.domain.DrawerItem import me.zobrist.tichucounter.domain.KeepScreenOn import me.zobrist.tichucounter.domain.Language @@ -54,6 +56,7 @@ import me.zobrist.tichucounter.domain.Theme import me.zobrist.tichucounter.domain.TopBarAction import me.zobrist.tichucounter.domain.TopBarState import me.zobrist.tichucounter.domain.navigate +import me.zobrist.tichucounter.repository.GameRepository import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.MainViewModel import me.zobrist.tichucounter.ui.about.AboutView @@ -74,6 +77,9 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var settingsAdapter: SettingsAdapter + @Inject + lateinit var repository: GameRepository + @Inject lateinit var reviewService: ReviewService @@ -82,6 +88,7 @@ class MainActivity : AppCompatActivity() { private val settingsViewModel: SettingsViewModel by viewModels() private val mainViewModel: MainViewModel by viewModels() + private var newGame: Game? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -107,8 +114,15 @@ class MainActivity : AppCompatActivity() { } } - mainViewModel.onNewGame = { - reviewService.request() + lifecycleScope.launch { + repository.getNewGameStarted().collect{ + if(newGame == null) + { + newGame = it + return@collect + } + reviewService.request() + } } setContent { @@ -252,7 +266,7 @@ class MainActivity : AppCompatActivity() { expanded = false it?.let { when (it) { - newGameTranslated -> mainViewModel.newGame() + newGameTranslated -> lifecycleScope.launch { repository.newGame() } } } } diff --git a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt index ce01b3c..e76558d 100644 --- a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt +++ b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt @@ -3,6 +3,7 @@ package me.zobrist.tichucounter.repository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.take @@ -13,6 +14,7 @@ import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.RoundDao import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round +import me.zobrist.tichucounter.domain.KeepScreenOn import java.util.Date import javax.inject.Inject @@ -23,6 +25,8 @@ class GameRepository @Inject constructor( private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date()) + private val newGameFlow = MutableStateFlow(Game()) + init { CoroutineScope(Dispatchers.IO).launch { gameDao.getActiveAsFlow().collect { @@ -39,6 +43,7 @@ class GameRepository @Inject constructor( withContext(Dispatchers.IO) { val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date())) setActive(id) + newGameFlow.value= gameDao.getGameById(id) } } @@ -134,4 +139,9 @@ class GameRepository @Inject constructor( fun getDistinctTeamNames(): Flow> { return gameDao.getDistinctTeamNames() } + + fun getNewGameStarted(): Flow + { + return newGameFlow + } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt index db9d2b4..09a1acc 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt @@ -7,7 +7,9 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.repository.GameRepository import javax.inject.Inject @@ -21,8 +23,6 @@ class MainViewModel @Inject constructor( private var redoRounds = mutableStateListOf() private var expectedRoundCount = 0 - var onNewGame: (() -> Unit)? = null - var isUndoActionActive by mutableStateOf(false) val isRedoActionActive: Boolean @@ -31,6 +31,8 @@ class MainViewModel @Inject constructor( var activeGameHasRounds by mutableStateOf(false) private set + private var newGame: Game? = null + init { viewModelScope.launch { @@ -47,6 +49,17 @@ class MainViewModel @Inject constructor( expectedRoundCount = it.rounds.count() } } + + viewModelScope.launch { + gameRepository.getNewGameStarted().collect { + if(newGame == null) + { + newGame = it + return@collect + } + redoRounds.clear() + } + } } fun undoLastRound() { @@ -71,12 +84,4 @@ class MainViewModel @Inject constructor( } } } - - fun newGame() { - viewModelScope.launch { - redoRounds.clear() - gameRepository.newGame() - } - onNewGame?.let { it() } - } } \ No newline at end of file From c3c0f09313beae9702f5431b6e689a37c11edcd3 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 23 Sep 2023 11:42:57 +0200 Subject: [PATCH 22/28] Use game finished setting to display victory dialog. --- .../tichucounter/domain/SettingsAdapter.kt | 14 +++++++-- .../ui/counter/CounterViewModel.kt | 31 ++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt index 70acbbd..27a4a77 100644 --- a/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt +++ b/app/src/main/java/me/zobrist/tichucounter/domain/SettingsAdapter.kt @@ -21,6 +21,7 @@ enum class Language(val value: LocaleListCompat) { enum class KeepScreenOn(val value: Boolean) { ON(true), OFF(false) } typealias VictoryPoints = Int +typealias GameWon = Boolean @Singleton class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) { @@ -35,6 +36,8 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex val victoryPoints = MutableStateFlow(0) + val gameFinished = MutableStateFlow(false) + init { language.value = try { enumValueOf(sharedPreferences.getString(Language::class.simpleName, null)!!) @@ -56,6 +59,7 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex victoryPoints.value = sharedPreferences.getInt(VictoryPoints::class.simpleName, 1000) + gameFinished.value = sharedPreferences.getBoolean(GameWon::class.simpleName, false) CoroutineScope(Dispatchers.IO).launch { language.collect { @@ -80,6 +84,12 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex updatePreference(VictoryPoints::class.simpleName, it) } } + + CoroutineScope(Dispatchers.IO).launch { + gameFinished.collect { + updatePreference(GameWon::class.simpleName, it) + } + } } private fun updatePreference(name: String?, value: String) { @@ -88,9 +98,9 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex editor.apply() } - private fun updatePreference(name: String?, value: Long) { + private fun updatePreference(name: String?, value: Boolean) { val editor = sharedPreferences.edit() - editor.putLong(name, value) + editor.putBoolean(name, value) editor.apply() } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index a18f8a5..3dbd24f 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -6,10 +6,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.focus.FocusRequester import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round @@ -162,9 +164,11 @@ class CounterViewModel @Inject constructor( private var distinctTeamNames = listOf() - private var victoryDialogShown = false - private var lastGame: Game? = null + private var lastVictoryPoints: Int? = null + + private val gameWon: Boolean + get() = totalScoreA >= settings.victoryPoints.value || totalScoreB >= settings.victoryPoints.value init { viewModelScope.launch { @@ -182,12 +186,15 @@ class CounterViewModel @Inject constructor( buildTeamNameSuggestions() if (it.game.uid != lastGame?.uid) { - victoryDialogShown = false + if(lastGame != null) + { + settings.gameFinished.value = false + } lastGame = it.game } - if (!victoryDialogShown) { - if (totalScoreA >= settings.victoryPoints.value || totalScoreB >= settings.victoryPoints.value) { + if (!settings.gameFinished.value) { + if (gameWon) { showVictoryDialog = true } } @@ -201,9 +208,17 @@ class CounterViewModel @Inject constructor( buildTeamNameSuggestions() } - + } + viewModelScope.launch { settings.victoryPoints.collect { - victoryDialogShown = false + if(lastVictoryPoints == null) + { + lastVictoryPoints = it + return@collect + } + + // Game was already won and will be won also with new settings + settings.gameFinished.value = settings.gameFinished.value && gameWon } } } @@ -316,7 +331,7 @@ class CounterViewModel @Inject constructor( override fun victoryDialogExecuted(result: Boolean) { showVictoryDialog = false - victoryDialogShown = true + settings.gameFinished.value = true if (result) { viewModelScope.launch { From 1a2002e812ad1dbcd5dd06162bde1e2af55234b9 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 23 Sep 2023 11:48:43 +0200 Subject: [PATCH 23/28] Improve translations. --- .../main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt | 2 +- app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt index 1c6c68d..9288313 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterView.kt @@ -155,7 +155,7 @@ fun GameVictoryDialog( onDismissRequest = { onDismiss() }, dismissButton = { TextButton({ onDismiss() }) { - Text(stringResource(R.string.continue_play)) + Text(stringResource(R.string.abort)) } }, confirmButton = { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index be1fb4e..ec33beb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -36,4 +36,5 @@ Sieht aus, als ob ihr ein neues Spiel starten solltet, um das endgültig zu klären. Unentschieden Herzliche Gratulation! Wie wäre es mit einer Revanche? + Abbrechen \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f477b30..1cb4440 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,4 +40,5 @@ Looks like you should start a new game to settle this for good. Draw Congratulations! How about a rematch? + Abort \ No newline at end of file From c968d2ee91cb8214ca02a862148dc9abfd829344 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 23 Sep 2023 12:06:33 +0200 Subject: [PATCH 24/28] Improve coloring and padding of history list. --- .../tichucounter/ui/history/HistoryView.kt | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index c3fff06..266cc43 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -168,7 +168,7 @@ fun HistoryList( if (it.game.active) { HistoryListItem( it, - Modifier.animateItemPlacement() + Modifier.animateItemPlacement().padding(2.dp) ) } else { DismissibleHistoryListItem( @@ -235,11 +235,20 @@ fun DismissibleHistoryListItem( val direction = dismissState.dismissDirection ?: return@SwipeToDismiss val color by animateColorAsState( when (dismissState.targetValue) { - DismissValue.DismissedToStart -> Color.Red + DismissValue.DismissedToStart -> MaterialTheme.colorScheme.error + DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.primary else -> MaterialTheme.colorScheme.background }, label = "" ) + val textColor by animateColorAsState( + when (dismissState.targetValue) { + DismissValue.DismissedToStart -> MaterialTheme.colorScheme.onError + DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.onPrimary + else -> MaterialTheme.colorScheme.onBackground + + }, label = "" + ) val alignment = when (direction) { DismissDirection.StartToEnd -> Alignment.CenterStart DismissDirection.EndToStart -> Alignment.CenterEnd @@ -259,8 +268,9 @@ fun DismissibleHistoryListItem( Box( Modifier .fillMaxSize() + .padding(top=2.dp, bottom = 2.dp) .background(color) - .padding(horizontal = 20.dp), + .padding(horizontal = 10.dp), contentAlignment = alignment ) { Column( @@ -268,13 +278,13 @@ fun DismissibleHistoryListItem( horizontalAlignment = Alignment.CenterHorizontally ) { Icon( - icon, contentDescription = null, modifier = Modifier.scale(scale) + icon, contentDescription = null, modifier = Modifier.scale(scale), tint = textColor ) - Text(text = text) + Text(text = text, color = textColor) } } }, dismissContent = { - HistoryListItem(game = game) + HistoryListItem(game = game, modifier= Modifier.padding(2.dp)) }) } @@ -291,7 +301,6 @@ fun HistoryListItem( Card( modifier = modifier .fillMaxWidth() - .padding(all = 4.dp) ) { Row( Modifier.padding(all = 12.dp) From 44246e329e16489a21f3c813dc6fad77e72fcb27 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Sat, 23 Sep 2023 13:41:26 +0200 Subject: [PATCH 25/28] Request review on victory dialog closed. Reformat code. closes: #51 --- .../java/me/zobrist/tichucounter/MainActivity.kt | 15 +++++++-------- .../tichucounter/repository/GameRepository.kt | 9 ++++----- .../me/zobrist/tichucounter/ui/MainViewModel.kt | 4 +--- .../tichucounter/ui/counter/CounterViewModel.kt | 8 ++------ .../tichucounter/ui/history/HistoryView.kt | 14 +++++++++----- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt index 6d80823..72fe2bb 100644 --- a/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt +++ b/app/src/main/java/me/zobrist/tichucounter/MainActivity.kt @@ -43,9 +43,7 @@ import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.domain.DrawerItem import me.zobrist.tichucounter.domain.KeepScreenOn import me.zobrist.tichucounter.domain.Language @@ -88,7 +86,7 @@ class MainActivity : AppCompatActivity() { private val settingsViewModel: SettingsViewModel by viewModels() private val mainViewModel: MainViewModel by viewModels() - private var newGame: Game? = null + private var requestReview: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -115,13 +113,14 @@ class MainActivity : AppCompatActivity() { } lifecycleScope.launch { - repository.getNewGameStarted().collect{ - if(newGame == null) - { - newGame = it + settingsAdapter.gameFinished.collect { + if (!requestReview) { + requestReview = true return@collect } - reviewService.request() + if (it) { + reviewService.request() + } } } diff --git a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt index e76558d..40b02df 100644 --- a/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt +++ b/app/src/main/java/me/zobrist/tichucounter/repository/GameRepository.kt @@ -14,7 +14,6 @@ import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.RoundDao import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round -import me.zobrist.tichucounter.domain.KeepScreenOn import java.util.Date import javax.inject.Inject @@ -23,7 +22,8 @@ class GameRepository @Inject constructor( private val roundDao: RoundDao ) { - private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date()) + var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date()) + private set private val newGameFlow = MutableStateFlow(Game()) @@ -43,7 +43,7 @@ class GameRepository @Inject constructor( withContext(Dispatchers.IO) { val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date())) setActive(id) - newGameFlow.value= gameDao.getGameById(id) + newGameFlow.value = gameDao.getGameById(id) } } @@ -140,8 +140,7 @@ class GameRepository @Inject constructor( return gameDao.getDistinctTeamNames() } - fun getNewGameStarted(): Flow - { + fun getNewGameStarted(): Flow { return newGameFlow } } \ No newline at end of file diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt index 09a1acc..87d1232 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/MainViewModel.kt @@ -7,7 +7,6 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round @@ -52,8 +51,7 @@ class MainViewModel @Inject constructor( viewModelScope.launch { gameRepository.getNewGameStarted().collect { - if(newGame == null) - { + if (newGame == null) { newGame = it return@collect } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index 3dbd24f..c3b8b9a 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -6,12 +6,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.focus.FocusRequester import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Round @@ -186,8 +184,7 @@ class CounterViewModel @Inject constructor( buildTeamNameSuggestions() if (it.game.uid != lastGame?.uid) { - if(lastGame != null) - { + if (lastGame != null) { settings.gameFinished.value = false } lastGame = it.game @@ -211,8 +208,7 @@ class CounterViewModel @Inject constructor( } viewModelScope.launch { settings.victoryPoints.collect { - if(lastVictoryPoints == null) - { + if (lastVictoryPoints == null) { lastVictoryPoints = it return@collect } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index 266cc43..17cf750 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.TopEnd import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -168,7 +167,9 @@ fun HistoryList( if (it.game.active) { HistoryListItem( it, - Modifier.animateItemPlacement().padding(2.dp) + Modifier + .animateItemPlacement() + .padding(2.dp) ) } else { DismissibleHistoryListItem( @@ -268,7 +269,7 @@ fun DismissibleHistoryListItem( Box( Modifier .fillMaxSize() - .padding(top=2.dp, bottom = 2.dp) + .padding(top = 2.dp, bottom = 2.dp) .background(color) .padding(horizontal = 10.dp), contentAlignment = alignment @@ -278,13 +279,16 @@ fun DismissibleHistoryListItem( horizontalAlignment = Alignment.CenterHorizontally ) { Icon( - icon, contentDescription = null, modifier = Modifier.scale(scale), tint = textColor + icon, + contentDescription = null, + modifier = Modifier.scale(scale), + tint = textColor ) Text(text = text, color = textColor) } } }, dismissContent = { - HistoryListItem(game = game, modifier= Modifier.padding(2.dp)) + HistoryListItem(game = game, modifier = Modifier.padding(2.dp)) }) } From 461dc4a30cbd9b86e75eeafcb6074da39d4031bf Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Wed, 27 Sep 2023 18:30:03 +0200 Subject: [PATCH 26/28] Reset gameFinished on removed round. closes: #52 --- .../tichucounter/ui/counter/CounterViewModel.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index c3b8b9a..651d5db 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -168,6 +168,7 @@ class CounterViewModel @Inject constructor( private val gameWon: Boolean get() = totalScoreA >= settings.victoryPoints.value || totalScoreB >= settings.victoryPoints.value + private var lastRoundCount: Int = 0 init { viewModelScope.launch { gameRepository.getActiveGameFlow().collect { @@ -183,18 +184,30 @@ class CounterViewModel @Inject constructor( buildTeamNameSuggestions() + // Game has changed if (it.game.uid != lastGame?.uid) { if (lastGame != null) { settings.gameFinished.value = false } lastGame = it.game + lastRoundCount = it.rounds.size } + // Game winning condition if (!settings.gameFinished.value) { if (gameWon) { showVictoryDialog = true } } + + // Undo game winning if rounds were removed + if(lastRoundCount > it.rounds.size) + { + if(!gameWon){ + settings.gameFinished.value = false + } + } + lastRoundCount = it.rounds.size } } } From fa9786de046ebf17503bd3474f9415ee6696f4e6 Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Wed, 27 Sep 2023 18:51:26 +0200 Subject: [PATCH 27/28] Scroll to top on game activated. --- .../tichucounter/ui/history/HistoryView.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index 17cf750..c7fb1b1 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -53,6 +53,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.zobrist.tichucounter.R import me.zobrist.tichucounter.data.GameWithScores @@ -159,23 +161,31 @@ fun HistoryList( onDeleteAllClicked: () -> Unit, lazyListState: LazyListState = LazyListState(), ) { + val scope = rememberCoroutineScope() + Row { LazyColumn(state = lazyListState) { items( items = games, - key = { it.hashCode() }) { - if (it.game.active) { + key = { it.game.uid }) { item -> + if (item.game.active) { HistoryListItem( - it, + item, Modifier .animateItemPlacement() .padding(2.dp) ) } else { DismissibleHistoryListItem( - it, + item, Modifier.animateItemPlacement(), - onOpenClicked, + { + onOpenClicked(it) + scope.launch { + delay(100) + lazyListState.animateScrollToItem(0) + } + }, onDeleteClicked ) } From 45ae61584d9d58fe43e9a49f4b5590866fbe385d Mon Sep 17 00:00:00 2001 From: Fabian Zobrist Date: Wed, 27 Sep 2023 18:53:05 +0200 Subject: [PATCH 28/28] Prevent winning dialog on game selected with sufficient points. --- .../me/zobrist/tichucounter/ui/counter/CounterViewModel.kt | 7 ++++--- .../java/me/zobrist/tichucounter/ui/history/HistoryView.kt | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt index 651d5db..8d2c5c9 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/counter/CounterViewModel.kt @@ -169,6 +169,7 @@ class CounterViewModel @Inject constructor( get() = totalScoreA >= settings.victoryPoints.value || totalScoreB >= settings.victoryPoints.value private var lastRoundCount: Int = 0 + init { viewModelScope.launch { gameRepository.getActiveGameFlow().collect { @@ -191,6 +192,7 @@ class CounterViewModel @Inject constructor( } lastGame = it.game lastRoundCount = it.rounds.size + return@collect } // Game winning condition @@ -201,9 +203,8 @@ class CounterViewModel @Inject constructor( } // Undo game winning if rounds were removed - if(lastRoundCount > it.rounds.size) - { - if(!gameWon){ + if (lastRoundCount > it.rounds.size) { + if (!gameWon) { settings.gameFinished.value = false } } diff --git a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt index c7fb1b1..cb4cfd7 100644 --- a/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt +++ b/app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt @@ -53,7 +53,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.zobrist.tichucounter.R