44 Commits

Author SHA1 Message Date
cf5c408464 Update dependencies to use newest Android SDK. Clean up.
All checks were successful
Build Android / build (pull_request) Successful in 2m52s
Build Android / build (push) Successful in 3m2s
2024-08-23 12:16:44 +02:00
fdf8c5c7df Update dependencies.
All checks were successful
Build Android / build (push) Successful in 2m57s
2024-08-23 12:02:58 +02:00
7ed3a29c5b Merge pull request 'feature/update-dependencies' (#55) from feature/update-dependencies into develop
All checks were successful
Build Android / build (push) Successful in 2m37s
Reviewed-on: #55
2024-06-06 13:43:27 +02:00
9434d1f1b9 Upload as gitea artifacts.
All checks were successful
Build Android / build (push) Successful in 2m36s
Build Android / build (pull_request) Successful in 2m39s
2024-05-31 16:24:06 +02:00
9abff51cb8 Update deprecated swipe to dismiss box. 2024-05-31 15:49:29 +02:00
93f51b9220 Update dependencies. 2024-05-31 14:55:38 +02:00
8bb2b9bf20 Merge tag '2.3.2' into develop
All checks were successful
Build Android / build (push) Successful in 2m29s
2023-11-02 22:34:48 +01:00
00569666be Improve delete multiple games in a row.
All checks were successful
Build Android / build (push) Successful in 4m33s
closes #53
2023-11-02 22:32:35 +01:00
2cf6578378 Update gradle. 2023-11-02 22:32:31 +01:00
fbcf9af761 Change deploy user. 2023-11-02 22:32:24 +01:00
80039a5f94 Merge tag '2.3.1' into develop
All checks were successful
Build Android / build (push) Successful in 2m32s
2023-10-01 07:45:09 +02:00
1c6674373f Merge branch 'hotfix/2.3.1'
All checks were successful
Build Android / build (push) Successful in 9m21s
2023-10-01 07:45:09 +02:00
67e803f913 Revert "Scroll to top on game activated."
All checks were successful
Build Android / build (push) Successful in 8m44s
This reverts commit fa9786de04.

# Conflicts:
#	app/src/main/java/me/zobrist/tichucounter/ui/history/HistoryView.kt
2023-09-27 22:45:10 +02:00
4c01fe13a8 Merge tag '2.3' into develop
All checks were successful
Build Android / build (push) Successful in 9m4s
2023-09-27 19:05:39 +02:00
2d30de8855 Merge branch 'release/2.3'
All checks were successful
Build Android / build (push) Successful in 8m34s
2023-09-27 19:05:39 +02:00
45ae61584d Prevent winning dialog on game selected with sufficient points.
All checks were successful
Build Android / build (push) Successful in 8m36s
2023-09-27 18:53:05 +02:00
fa9786de04 Scroll to top on game activated. 2023-09-27 18:51:26 +02:00
461dc4a30c Reset gameFinished on removed round.
closes: #52
2023-09-27 18:30:03 +02:00
44246e329e Request review on victory dialog closed. Reformat code.
All checks were successful
Build Android / build (push) Successful in 9m12s
closes: #51
2023-09-23 13:42:24 +02:00
c968d2ee91 Improve coloring and padding of history list.
All checks were successful
Build Android / build (push) Successful in 8m39s
2023-09-23 12:06:33 +02:00
1a2002e812 Improve translations. 2023-09-23 11:48:43 +02:00
c3c0f09313 Use game finished setting to display victory dialog. 2023-09-23 11:42:57 +02:00
81a0d680e9 Flow on new game.
Now review is always requested regardless of where the new game was started.
2023-09-08 18:40:52 +02:00
27cb2f670b Settings as state flow. 2023-09-08 18:20:56 +02:00
6a96749501 Increase version.
All checks were successful
Build Android / build (push) Successful in 8m33s
2023-09-01 18:31:21 +02:00
2fa4e218e5 Merge pull request 'feature/winning-points' (#50) from feature/winning-points into develop
All checks were successful
Build Android / build (push) Successful in 8m16s
Reviewed-on: #50
2023-09-01 18:30:29 +02:00
2599e3320a Add alerts for winning and draw.
All checks were successful
Build Android / build (push) Successful in 8m35s
Build Android / build (pull_request) Successful in 8m23s
2023-09-01 17:42:03 +02:00
c97d98704b Update view with victory points setting.
Add victory points to settings adapter.
2023-09-01 16:27:58 +02:00
37bbf45ce0 Merge pull request 'feature/store-review' (#49) from feature/store-review into develop
All checks were successful
Build Android / build (push) Successful in 8m31s
Reviewed-on: #49
2023-09-01 16:03:26 +02:00
dbef6e047a Request review only every third game after three months.
All checks were successful
Build Android / build (push) Successful in 8m29s
Build Android / build (pull_request) Has been cancelled
2023-09-01 13:00:41 +02:00
450c8a1547 Should request a review every new game.
All checks were successful
Build Android / build (push) Successful in 8m24s
2023-09-01 12:24:18 +02:00
536b5b7fd2 Merge pull request 'feature/android-14' (#48) from feature/android-14 into develop
All checks were successful
Build Android / build (push) Successful in 8m21s
Reviewed-on: #48
2023-09-01 11:07:46 +02:00
57b02ce74a Fix keyboard not hidden.
All checks were successful
Build Android / build (push) Successful in 8m14s
Build Android / build (pull_request) Successful in 8m4s
2023-09-01 10:57:57 +02:00
4e4653da97 Update to Sdk34 2023-09-01 10:57:57 +02:00
e5041c98e1 Merge pull request 'feature/swipe-actions' (#44) from feature/swipe-actions into develop
All checks were successful
Build Android / build (push) Successful in 7m26s
Reviewed-on: #44
2023-08-25 18:19:44 +02:00
8521247c58 Show all games in list but mark active game with an badge.
All checks were successful
Build Android / build (push) Successful in 7m50s
Build Android / build (pull_request) Successful in 7m27s
2023-08-25 15:59:58 +02:00
7108af4cf4 Fix typo.
All checks were successful
Build Android / build (push) Successful in 7m44s
2023-08-20 22:22:33 +02:00
55a2293b6c Show snackbar with undo option. Don't jump back to calculator on history click.
All checks were successful
Build Android / build (push) Has been cancelled
2023-08-20 22:20:45 +02:00
c8098fc904 Increase dismiss threshold. Change translation of continue. 2023-08-20 12:31:20 +02:00
ec3b51051a Add swipe to open and delete to history items.
All checks were successful
Build Android / build (push) Successful in 7m17s
2023-08-19 18:35:16 +02:00
07219bffb4 Update dependencies. 2023-08-19 16:05:41 +02:00
1819802748 Merge pull request 'release/2.2' (#41) from release/2.2 into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #41
2023-06-16 11:11:22 +02:00
a2b84640b1 Merge pull request 'release/2.1' (#36) from release/2.1 into master
Some checks are pending
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is running
Reviewed-on: fabian/TichuCounter#36
2023-03-10 17:21:17 +01:00
30723f7862 Merge pull request 'release/2.0' (#21) from release/2.0 into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: fabian/TichuCounter#21
2023-01-28 23:29:25 +01:00
28 changed files with 850 additions and 621 deletions

View File

@@ -30,15 +30,11 @@ jobs:
- name: Build abb - name: Build abb
run: ./gradlew bundleRelease run: ./gradlew bundleRelease
- name: Deploy latest to Nextcloud - uses: actions/upload-artifact@v3
run: | with:
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" name: app-release
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" retention-days: 10
path: app/build/outputs/**/release/app-release.*
- 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 - uses: https://github.com/ravsamhq/notify-slack-action@v2
if: always() if: always()

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@
.idea .idea
keystore.properties keystore.properties
version.properties version.properties
.vscode/

View File

@@ -3,6 +3,7 @@ plugins {
id 'kotlin-android' id 'kotlin-android'
id 'com.google.dagger.hilt.android' id 'com.google.dagger.hilt.android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'com.google.devtools.ksp'
} }
// Create a variable called keystorePropertiesFile, and initialize it to your // Create a variable called keystorePropertiesFile, and initialize it to your
@@ -16,7 +17,7 @@ def keystoreProperties = new Properties()
def versionProperties = new Properties() def versionProperties = new Properties()
def versionMajor = 2 def versionMajor = 2
def versionMinor = 2 def versionMinor = 3
// Load your keystore.properties file into the keystoreProperties object. // Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
@@ -24,12 +25,12 @@ versionProperties.load(new FileInputStream(versionPropertiesFile))
android { android {
compileSdkVersion 33 compileSdk 34
defaultConfig { defaultConfig {
applicationId "me.zobrist.tichucounter" applicationId "me.zobrist.tichucounter"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 34
versionCode versionProperties["versionCode"].toInteger() versionCode versionProperties["versionCode"].toInteger()
versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}" versionName "${versionMajor}.${versionMinor}.${versionProperties["versionCode"].toInteger()}"
resourceConfigurations += ['de', 'en'] resourceConfigurations += ['de', 'en']
@@ -63,12 +64,11 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding = true
compose = true compose = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.4.7" kotlinCompilerExtensionVersion = "1.5.14"
} }
compileOptions { compileOptions {
@@ -89,45 +89,44 @@ android {
dependencies { dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation "androidx.compose.material3:material3:1.1.1" implementation "androidx.compose.material3:material3:1.2.1"
implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:core-ktx:1.8.1' implementation 'com.google.android.play:review-ktx:2.0.1'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
implementation 'androidx.fragment:fragment-ktx:1.6.0' implementation 'androidx.fragment:fragment-ktx:1.8.2'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.recyclerview:recyclerview:1.3.0' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
implementation 'androidx.compose.material:material-icons-extended:1.4.3' implementation 'androidx.compose.material:material-icons-extended:1.6.8'
implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0" implementation "com.google.accompanist:accompanist-systemuicontroller:0.27.0"
implementation 'androidx.activity:activity-compose:1.7.2' implementation 'androidx.activity:activity-compose:1.9.1'
implementation "androidx.compose.ui:ui:1.4.3" implementation "androidx.compose.ui:ui:1.6.8"
implementation "androidx.compose.ui:ui-tooling-preview:1.4.3" implementation "androidx.compose.ui:ui-tooling-preview:1.6.8"
implementation "androidx.compose.runtime:runtime-livedata:1.4.3" implementation "androidx.compose.runtime:runtime-livedata:1.6.8"
implementation "androidx.navigation:navigation-compose:2.6.0" implementation "androidx.navigation:navigation-compose:2.7.7"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
implementation "com.google.dagger:hilt-android:2.44" implementation "com.google.dagger:hilt-android:2.51.1"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.4.3" androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.8"
debugImplementation "androidx.compose.ui:ui-tooling:1.4.3" debugImplementation "androidx.compose.ui:ui-tooling:1.6.8"
debugImplementation "androidx.compose.ui:ui-test-manifest:1.4.3" debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.8"
kapt "com.google.dagger:hilt-compiler:2.44" kapt "com.google.dagger:hilt-compiler:2.51.1"
implementation "androidx.room:room-runtime:2.5.2" annotationProcessor "androidx.room:room-compiler:2.6.1"
annotationProcessor "androidx.room:room-compiler:2.5.2" implementation "androidx.room:room-runtime:2.6.1"
kapt "androidx.room:room-compiler:2.5.2" ksp "androidx.room:room-compiler:2.6.1"
implementation "androidx.room:room-ktx:2.5.2" implementation "androidx.room:room-ktx:2.6.1"
implementation "androidx.multidex:multidex:2.0.1" implementation "androidx.multidex:multidex:2.0.1"
api "androidx.navigation:navigation-fragment-ktx:2.6.0" api "androidx.navigation:navigation-fragment-ktx:2.7.7"
implementation "com.patrykandpatrick.vico:compose-m3:1.6.5"
} }
// Allow references to generated code // Allow references to generated code

View File

@@ -8,14 +8,14 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.List
import androidx.compose.material.icons.automirrored.outlined.Redo
import androidx.compose.material.icons.automirrored.outlined.Undo
import androidx.compose.material.icons.outlined.Calculate import androidx.compose.material.icons.outlined.Calculate
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.outlined.Keyboard 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.MoreVert
import androidx.compose.material.icons.outlined.Redo
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Undo
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
@@ -23,6 +23,8 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -32,9 +34,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -42,16 +45,16 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.domain.DrawerItem import me.zobrist.tichucounter.domain.DrawerItem
import me.zobrist.tichucounter.domain.ISystemSettingsListener
import me.zobrist.tichucounter.domain.KeepScreenOn import me.zobrist.tichucounter.domain.KeepScreenOn
import me.zobrist.tichucounter.domain.Language import me.zobrist.tichucounter.domain.Language
import me.zobrist.tichucounter.domain.ReviewService
import me.zobrist.tichucounter.domain.Route import me.zobrist.tichucounter.domain.Route
import me.zobrist.tichucounter.domain.SettingsAdapter import me.zobrist.tichucounter.domain.SettingsAdapter
import me.zobrist.tichucounter.domain.Theme import me.zobrist.tichucounter.domain.Theme
import me.zobrist.tichucounter.domain.TopBarAction import me.zobrist.tichucounter.domain.TopBarAction
import me.zobrist.tichucounter.domain.TopBarState import me.zobrist.tichucounter.domain.TopBarState
import me.zobrist.tichucounter.domain.composable
import me.zobrist.tichucounter.domain.navigate import me.zobrist.tichucounter.domain.navigate
import me.zobrist.tichucounter.repository.GameRepository
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
import me.zobrist.tichucounter.ui.MainViewModel import me.zobrist.tichucounter.ui.MainViewModel
import me.zobrist.tichucounter.ui.about.AboutView import me.zobrist.tichucounter.ui.about.AboutView
@@ -67,21 +70,59 @@ import me.zobrist.tichucounter.ui.settings.SettingsViewModel
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(), ISystemSettingsListener { class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var settingsAdapter: SettingsAdapter lateinit var settingsAdapter: SettingsAdapter
@Inject
lateinit var repository: GameRepository
@Inject
lateinit var reviewService: ReviewService
private val counterViewModel: CounterViewModel by viewModels() private val counterViewModel: CounterViewModel by viewModels()
private val historyViewModel: HistoryViewModel by viewModels() private val historyViewModel: HistoryViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels()
private val mainViewModel: MainViewModel by viewModels() private val mainViewModel: MainViewModel by viewModels()
private var requestReview: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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)
}
}
lifecycleScope.launch {
settingsAdapter.gameFinished.collect {
if (!requestReview) {
requestReview = true
return@collect
}
if (it) {
reviewService.request()
}
}
}
setContent { setContent {
AppTheme { AppTheme {
@@ -92,16 +133,11 @@ class MainActivity : AppCompatActivity(), ISystemSettingsListener {
} }
} }
override fun onDestroy() { private fun changeLanguage(language: Language) {
super.onDestroy()
settingsAdapter.unregisterOnChangeListener(this)
}
override fun onLanguageChanged(language: Language) {
AppCompatDelegate.setApplicationLocales(language.value) AppCompatDelegate.setApplicationLocales(language.value)
} }
override fun onThemeChanged(theme: Theme) { private fun changeTheme(theme: Theme) {
val themeValue = when (theme) { val themeValue = when (theme) {
Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO Theme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES Theme.DARK -> AppCompatDelegate.MODE_NIGHT_YES
@@ -110,7 +146,7 @@ class MainActivity : AppCompatActivity(), ISystemSettingsListener {
AppCompatDelegate.setDefaultNightMode(themeValue) AppCompatDelegate.setDefaultNightMode(themeValue)
} }
override fun onScreenOnChanged(keepOn: KeepScreenOn) { private fun setKeepScreenOn(keepOn: KeepScreenOn) {
if (keepOn.value) { if (keepOn.value) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else { } else {
@@ -130,7 +166,11 @@ class MainActivity : AppCompatActivity(), ISystemSettingsListener {
Icons.Outlined.Calculate, Icons.Outlined.Calculate,
stringResource(R.string.menu_counter) stringResource(R.string.menu_counter)
), ),
DrawerItem(Route.HISTORY, Icons.Outlined.List, stringResource(R.string.menu_history)), DrawerItem(
Route.HISTORY,
Icons.AutoMirrored.Outlined.List,
stringResource(R.string.menu_history)
),
DrawerItem( DrawerItem(
Route.SETTINGS, Route.SETTINGS,
Icons.Outlined.Settings, Icons.Outlined.Settings,
@@ -181,12 +221,13 @@ class MainActivity : AppCompatActivity(), ISystemSettingsListener {
) { ) {
var topBarState by remember { mutableStateOf(TopBarState()) } var topBarState by remember { mutableStateOf(TopBarState()) }
val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) }
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = { floatingActionButton = {
if (showFab) { if (showFab) {
FloatingActionButton( FloatingActionButton(
modifier = Modifier.padding(end = 75.dp),
onClick = { fabAction() }) { onClick = { fabAction() }) {
Icon(Icons.Outlined.Keyboard, null) Icon(Icons.Outlined.Keyboard, null)
} }
@@ -199,7 +240,7 @@ class MainActivity : AppCompatActivity(), ISystemSettingsListener {
startDestination = Route.COUNTER.name, startDestination = Route.COUNTER.name,
modifier = Modifier.padding(paddings) modifier = Modifier.padding(paddings)
) { ) {
composable(Route.COUNTER) { this.composable(Route.COUNTER.name) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
@@ -207,11 +248,11 @@ class MainActivity : AppCompatActivity(), ISystemSettingsListener {
title = stringResource(R.string.app_name), title = stringResource(R.string.app_name),
actions = (listOf( actions = (listOf(
TopBarAction( TopBarAction(
Icons.Outlined.Undo, Icons.AutoMirrored.Outlined.Undo,
mainViewModel.isUndoActionActive, mainViewModel.isUndoActionActive,
{ mainViewModel.undoLastRound() }), { mainViewModel.undoLastRound() }),
TopBarAction( TopBarAction(
Icons.Outlined.Redo, Icons.AutoMirrored.Outlined.Redo,
mainViewModel.isRedoActionActive, mainViewModel.isRedoActionActive,
{ mainViewModel.redoLastRound() }), { mainViewModel.redoLastRound() }),
TopBarAction( TopBarAction(
@@ -219,15 +260,16 @@ class MainActivity : AppCompatActivity(), ISystemSettingsListener {
mainViewModel.activeGameHasRounds, mainViewModel.activeGameHasRounds,
{ expanded = true } { expanded = true }
) { ) {
val newGameTranslated = stringResource(R.string.new_game)
DropDownMenu( DropDownMenu(
mapOf("new" to R.string.newGame), listOf(newGameTranslated),
"", "",
expanded, expanded,
) { ) {
expanded = false expanded = false
it?.let { it?.let {
when (it) { when (it) {
"new" -> mainViewModel.newGame() newGameTranslated -> lifecycleScope.launch { repository.newGame() }
} }
} }
} }
@@ -243,20 +285,23 @@ class MainActivity : AppCompatActivity(), ISystemSettingsListener {
Counter(counterViewModel) Counter(counterViewModel)
} }
composable(Route.HISTORY) { composable(Route.HISTORY.name) {
topBarState = topBarState =
TopBarState(title = stringResource(R.string.menu_history)) { scope.launch { drawerState.open() } } 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) { composable(Route.SETTINGS.name) {
topBarState = topBarState =
TopBarState(title = stringResource(R.string.menu_settings)) { scope.launch { drawerState.open() } } TopBarState(title = stringResource(R.string.menu_settings)) { scope.launch { drawerState.open() } }
SettingsView(settingsViewModel) SettingsView(settingsViewModel)
} }
composable(Route.ABOUT) { composable(Route.ABOUT.name) {
topBarState = topBarState =
TopBarState(title = stringResource(R.string.menu_about)) { scope.launch { drawerState.open() } } TopBarState(title = stringResource(R.string.menu_about)) { scope.launch { drawerState.open() } }

View File

@@ -1,6 +1,8 @@
package me.zobrist.tichucounter.data package me.zobrist.tichucounter.data
import androidx.room.* import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Game

View File

@@ -1,6 +1,7 @@
package me.zobrist.tichucounter.data package me.zobrist.tichucounter.data
import androidx.room.* import androidx.room.Dao
import androidx.room.Query
import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.data.entity.Round
@Dao @Dao

View File

@@ -1,12 +1,6 @@
package me.zobrist.tichucounter.domain 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.NavController
import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
fun NavController.navigate(route: Route) { fun NavController.navigate(route: Route) {
this.navigate(route.name) { this.navigate(route.name) {
@@ -23,12 +17,3 @@ fun NavController.navigate(route: Route) {
restoreState = true restoreState = true
} }
} }
fun NavGraphBuilder.composable(
route: Route,
arguments: List<NamedNavArgument> = emptyList(),
deepLinks: List<NavDeepLink> = emptyList(),
content: @Composable (NavBackStackEntry) -> Unit
) {
this.composable(route.name, arguments, deepLinks, content)
}

View File

@@ -0,0 +1,55 @@
package me.zobrist.tichucounter.domain
import android.app.Activity
import android.content.Context
import androidx.preference.PreferenceManager
import com.google.android.play.core.review.ReviewManagerFactory
import dagger.hilt.android.qualifiers.ActivityContext
import java.util.Date
import javax.inject.Inject
class ReviewService @Inject constructor(@ActivityContext private val appContext: Context) {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext)
private val THREE_MONTHS: Long = 7776000000
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()
editor.putLong("lastReviewedDate", value.time)
editor.apply()
}
fun request() {
requestCalled += 1
if (requestCalled >= 3) {
if (nextReviewedDate.time < System.currentTimeMillis()) {
requestCalled = 0
nextReviewedDate = Date(System.currentTimeMillis() + THREE_MONTHS)
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)
}
}
}
}
}
}

View File

@@ -4,6 +4,10 @@ import android.content.Context
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -14,113 +18,78 @@ enum class Language(val value: LocaleListCompat) {
GERMAN(LocaleListCompat.forLanguageTags("de")) GERMAN(LocaleListCompat.forLanguageTags("de"))
} }
enum class CounterMode { LIST, GRAPH }
enum class KeepScreenOn(val value: Boolean) { ON(true), OFF(false) } enum class KeepScreenOn(val value: Boolean) { ON(true), OFF(false) }
interface ISystemSettingsListener { typealias VictoryPoints = Int
fun onLanguageChanged(language: Language) typealias GameWon = Boolean
fun onThemeChanged(theme: Theme)
fun onScreenOnChanged(keepOn: KeepScreenOn)
}
interface IDisplaySettingsListener {
fun onCounterModeChanged(counterMode: CounterMode)
}
@Singleton @Singleton
class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) { class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private var systemSettingsListeners = mutableListOf<ISystemSettingsListener>()
private var displaySettingsListeners = mutableListOf<IDisplaySettingsListener>()
var language: Language val language = MutableStateFlow(Language.DEFAULT)
private set
var theme: Theme val theme = MutableStateFlow(Theme.DEFAULT)
private set
var keepScreenOn: KeepScreenOn val keepScreenOn = MutableStateFlow(KeepScreenOn.OFF)
private set
var counterMode: CounterMode val victoryPoints = MutableStateFlow(0)
private set
val gameFinished = MutableStateFlow(false)
init { init {
language = try { language.value = try {
enumValueOf(sharedPreferences.getString(Language::class.simpleName, null)!!) enumValueOf(sharedPreferences.getString(Language::class.simpleName, null)!!)
} catch (_: Exception) { } catch (_: NullPointerException) {
Language.DEFAULT Language.DEFAULT
} }
theme = try { theme.value = try {
enumValueOf(sharedPreferences.getString(Theme::class.simpleName, null)!!) enumValueOf(sharedPreferences.getString(Theme::class.simpleName, null)!!)
} catch (_: Exception) { } catch (_: java.lang.Exception) {
Theme.DEFAULT Theme.DEFAULT
} }
keepScreenOn = try { keepScreenOn.value = try {
enumValueOf(sharedPreferences.getString(KeepScreenOn::class.simpleName, null)!!) enumValueOf(sharedPreferences.getString(KeepScreenOn::class.simpleName, null)!!)
} catch (_: Exception) { } catch (_: java.lang.Exception) {
KeepScreenOn.OFF KeepScreenOn.OFF
} }
counterMode = try { victoryPoints.value = sharedPreferences.getInt(VictoryPoints::class.simpleName, 1000)
enumValueOf(sharedPreferences.getString(CounterMode::class.simpleName, null)!!)
} catch (_: Exception) { gameFinished.value = sharedPreferences.getBoolean(GameWon::class.simpleName, false)
CounterMode.LIST
CoroutineScope(Dispatchers.IO).launch {
language.collect {
updatePreference(Language::class.simpleName, it.name)
} }
} }
fun registerOnChangeListener(listener: IDisplaySettingsListener) { CoroutineScope(Dispatchers.IO).launch {
displaySettingsListeners.add(listener) theme.collect {
updatePreference(Theme::class.simpleName, it.name)
listener.onCounterModeChanged(counterMode)
}
fun unregisterOnChangeListener(listener: IDisplaySettingsListener?) {
if (listener != null) {
displaySettingsListeners.remove(listener)
} }
} }
fun registerOnChangeListener(listener: ISystemSettingsListener) { CoroutineScope(Dispatchers.IO).launch {
systemSettingsListeners.add(listener) keepScreenOn.collect {
updatePreference(KeepScreenOn::class.simpleName, it.name)
listener.onThemeChanged(theme)
listener.onLanguageChanged(language)
listener.onScreenOnChanged(keepScreenOn)
}
fun unregisterOnChangeListener(listener: ISystemSettingsListener?) {
if (listener != null) {
systemSettingsListeners.remove(listener)
} }
} }
fun setLanguage(language: Language) { CoroutineScope(Dispatchers.IO).launch {
this.language = language victoryPoints.collect {
updatePreference(Language::class.simpleName, language.name) updatePreference(VictoryPoints::class.simpleName, it)
notifyListeners(language) }
} }
fun setTheme(theme: Theme) { CoroutineScope(Dispatchers.IO).launch {
this.theme = theme gameFinished.collect {
updatePreference(Theme::class.simpleName, theme.name) updatePreference(GameWon::class.simpleName, it)
notifyListeners(theme)
} }
fun setKeepScreenOn(setting: KeepScreenOn) {
this.keepScreenOn = setting
updatePreference(KeepScreenOn::class.simpleName, setting.name)
notifyListeners(setting)
} }
fun setCounterMode(counterMode: CounterMode) {
this.counterMode = counterMode
updatePreference(CounterMode::class.simpleName, counterMode.name)
notifyListeners(counterMode)
} }
private fun updatePreference(name: String?, value: String) { private fun updatePreference(name: String?, value: String) {
@@ -129,28 +98,15 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex
editor.apply() editor.apply()
} }
private fun notifyListeners(language: Language) { private fun updatePreference(name: String?, value: Boolean) {
systemSettingsListeners.forEach { val editor = sharedPreferences.edit()
it.onLanguageChanged(language) editor.putBoolean(name, value)
} editor.apply()
} }
private fun notifyListeners(theme: Theme) { private fun updatePreference(name: String?, value: Int) {
systemSettingsListeners.forEach { val editor = sharedPreferences.edit()
it.onThemeChanged(theme) editor.putInt(name, value)
editor.apply()
} }
}
private fun notifyListeners(keepScreenOn: KeepScreenOn) {
systemSettingsListeners.forEach {
it.onScreenOnChanged(keepScreenOn)
}
}
private fun notifyListeners(counterMode: CounterMode) {
displaySettingsListeners.forEach {
it.onCounterModeChanged(counterMode)
}
}
} }

View File

@@ -3,6 +3,7 @@ package me.zobrist.tichucounter.repository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
@@ -23,6 +24,10 @@ class GameRepository @Inject constructor(
private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date()) private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date())
private val newGameFlow = MutableStateFlow(Game())
private var deletedGame: GameWithScores? = null
init { init {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
gameDao.getActiveAsFlow().collect { gameDao.getActiveAsFlow().collect {
@@ -39,6 +44,7 @@ class GameRepository @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date())) val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date()))
setActive(id) setActive(id)
newGameFlow.value = gameDao.getGameById(id)
} }
} }
@@ -99,14 +105,33 @@ class GameRepository @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val game = gameDao.getGameById(uid) val game = gameDao.getGameById(uid)
gameDao.delete(game)
val rounds = roundDao.getAllForGame(game.uid) val rounds = roundDao.getAllForGame(game.uid)
deletedGame = GameWithScores(game, rounds)
gameDao.delete(game)
roundDao.delete(rounds) roundDao.delete(rounds)
} catch (_: NullPointerException) { } catch (_: NullPointerException) {
} }
} }
} }
suspend fun restoreLastDeletedGame() {
if (deletedGame == null) {
return
}
val revert = deletedGame!!
deletedGame = null
withContext(Dispatchers.IO) {
gameDao.insert(revert.game)
revert.rounds.forEach {
roundDao.insert(it)
}
}
}
suspend fun deleteAllInactive() { suspend fun deleteAllInactive() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
@@ -134,4 +159,8 @@ class GameRepository @Inject constructor(
fun getDistinctTeamNames(): Flow<List<String>> { fun getDistinctTeamNames(): Flow<List<String>> {
return gameDao.getDistinctTeamNames() return gameDao.getDistinctTeamNames()
} }
fun getNewGameStarted(): Flow<Game> {
return newGameFlow
}
} }

View File

@@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.repository.GameRepository import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject import javax.inject.Inject
@@ -29,6 +30,8 @@ class MainViewModel @Inject constructor(
var activeGameHasRounds by mutableStateOf(false) var activeGameHasRounds by mutableStateOf(false)
private set private set
private var newGame: Game? = null
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -45,6 +48,16 @@ class MainViewModel @Inject constructor(
expectedRoundCount = it.rounds.count() expectedRoundCount = it.rounds.count()
} }
} }
viewModelScope.launch {
gameRepository.getNewGameStarted().collect {
if (newGame == null) {
newGame = it
return@collect
}
redoRounds.clear()
}
}
} }
fun undoLastRound() { fun undoLastRound() {
@@ -69,11 +82,4 @@ class MainViewModel @Inject constructor(
} }
} }
} }
fun newGame() {
viewModelScope.launch {
redoRounds.clear()
gameRepository.newGame()
}
}
} }

View File

@@ -7,25 +7,29 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
@Composable @Composable
fun <T> DropDownMenu(map: Map<T, Int>, selected: T, expanded: Boolean, onSelected: (T?) -> Unit) { fun <T> DropDownMenu(
list: Collection<T>,
selected: T,
expanded: Boolean,
onSelected: (T?) -> Unit
) {
DropdownMenu( DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = { onSelected(null) } onDismissRequest = { onSelected(null) }
) { ) {
map.forEach { list.forEach {
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
onSelected(it.key) onSelected(it)
}, },
trailingIcon = { trailingIcon = {
if (it.key == selected) { if (it == selected) {
Icon(Icons.Outlined.Check, null) Icon(Icons.Outlined.Check, null)
} }
}, },
text = { Text(stringResource(it.value)) }, text = { Text(it.toString()) },
) )
} }
} }

View File

@@ -11,10 +11,10 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
@@ -26,7 +26,7 @@ import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TypeaheadTextField( fun TypeaheadTextField(
value: String, value: String,
@@ -46,7 +46,7 @@ fun TypeaheadTextField(
onExpandedChange = {} onExpandedChange = {}
) { ) {
var dropDownWidth by remember { mutableStateOf(0) } var dropDownWidth by remember { mutableIntStateOf(0) }
TextField( TextField(
value = value, value = value,

View File

@@ -1,40 +1,48 @@
package me.zobrist.tichucounter.ui.counter package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ShowChart import androidx.compose.material.icons.outlined.EmojiEvents
import androidx.compose.material.icons.outlined.TableRows import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.CounterMode
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
@Composable @Composable
fun Counter(viewModel: ICounterViewModel = PreviewViewModel()) { fun Counter(viewModel: ICounterViewModel = PreviewViewModel()) {
var orientation by remember { mutableStateOf(Configuration.ORIENTATION_PORTRAIT) } var orientation by remember { mutableIntStateOf(Configuration.ORIENTATION_PORTRAIT) }
orientation = LocalConfiguration.current.orientation orientation = LocalConfiguration.current.orientation
if (viewModel.showVictoryDialog) {
GameVictoryDialog(
viewModel.totalScoreA,
viewModel.totalScoreB,
viewModel.teamNameA,
viewModel.teamNameB,
{ viewModel.victoryDialogExecuted(false) })
{
viewModel.victoryDialogExecuted(true)
}
}
Surface { Surface {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) { if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
Landscape(viewModel) Landscape(viewModel)
@@ -62,13 +70,10 @@ fun Landscape(viewModel: ICounterViewModel) {
viewModel.totalScoreB viewModel.totalScoreB
) )
RoundView( RoundListView(
Modifier.weight(1f), viewModel.roundScoreList,
viewModel.counterMode, Modifier.weight(1f)
viewModel.roundScoreList
) )
{ viewModel.changeCounterMode() }
} }
if (!viewModel.keyboardHidden) { if (!viewModel.keyboardHidden) {
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
@@ -96,12 +101,10 @@ fun Portrait(viewModel: ICounterViewModel) {
viewModel.totalScoreB viewModel.totalScoreB
) )
RoundView( RoundListView(
Modifier.weight(1f), viewModel.roundScoreList,
viewModel.counterMode, Modifier.weight(1f)
viewModel.roundScoreList
) )
{ viewModel.changeCounterMode() }
if (!viewModel.keyboardHidden) { if (!viewModel.keyboardHidden) {
KeyBoardView(viewModel = viewModel) KeyBoardView(viewModel = viewModel)
@@ -109,41 +112,6 @@ fun Portrait(viewModel: ICounterViewModel) {
} }
} }
@Composable
private fun RoundView(
modifier: Modifier = Modifier,
counterMode: CounterMode,
rounds: List<Round>,
changeCounterMode: () -> Unit
) {
Box(modifier) {
if (counterMode == CounterMode.LIST) {
RoundListView(
rounds,
Modifier.fillMaxHeight()
)
} else {
RoundGraphView(
rounds,
Modifier.fillMaxHeight()
)
}
FloatingActionButton(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(15.dp),
onClick = { changeCounterMode() }) {
Icon(getIcon(counterMode), null)
}
}
}
private fun getIcon(counterMode: CounterMode): ImageVector {
return when (counterMode) {
CounterMode.LIST -> Icons.Outlined.ShowChart
CounterMode.GRAPH -> Icons.Outlined.TableRows
}
}
@Preview(name = "Light Mode") @Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true) @Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@@ -154,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.abort))
}
},
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 { internal class PreviewViewModel : ICounterViewModel {
override var roundScoreList: List<Round> = override var roundScoreList: List<Round> =
listOf(Round(1, 10, 90), Round(1, 50, 50), Round(1, 70, 30)) listOf(Round(1, 10, 90), Round(1, 50, 50), Round(1, 70, 30))
@@ -175,7 +191,7 @@ internal class PreviewViewModel : ICounterViewModel {
listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long") listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long")
override val teamNameSuggestionsB: List<String> = override val teamNameSuggestionsB: List<String> =
listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long") listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long")
override val counterMode: CounterMode = CounterMode.GRAPH override var showVictoryDialog: Boolean = false
override fun focusLastInput() { override fun focusLastInput() {
} }
@@ -208,7 +224,7 @@ internal class PreviewViewModel : ICounterViewModel {
override fun updateNameB(value: String) { override fun updateNameB(value: String) {
} }
override fun changeCounterMode() { override fun victoryDialogExecuted(result: Boolean) {
} }
override fun updateFocusStateA(state: Boolean) { override fun updateFocusStateA(state: Boolean) {

View File

@@ -1,6 +1,7 @@
package me.zobrist.tichucounter.ui.counter package me.zobrist.tichucounter.ui.counter
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@@ -10,9 +11,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.CounterMode
import me.zobrist.tichucounter.domain.IDisplaySettingsListener
import me.zobrist.tichucounter.domain.SettingsAdapter import me.zobrist.tichucounter.domain.SettingsAdapter
import me.zobrist.tichucounter.domain.Tichu import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.domain.digitCount import me.zobrist.tichucounter.domain.digitCount
@@ -60,27 +60,27 @@ interface ICounterViewModel : IKeyBoardViewModel {
val teamNameB: String val teamNameB: String
val teamNameSuggestionsA: List<String> val teamNameSuggestionsA: List<String>
val teamNameSuggestionsB: List<String> val teamNameSuggestionsB: List<String>
val counterMode: CounterMode val showVictoryDialog: Boolean
fun updateNameA(value: String) fun updateNameA(value: String)
fun updateNameB(value: String) fun updateNameB(value: String)
fun changeCounterMode() fun victoryDialogExecuted(result: Boolean)
} }
@HiltViewModel @HiltViewModel
class CounterViewModel @Inject constructor( class CounterViewModel @Inject constructor(
private val gameRepository: GameRepository, private val gameRepository: GameRepository,
private val settingsAdapter: SettingsAdapter private val settings: SettingsAdapter
) : ) :
ViewModel(), ICounterViewModel, IDisplaySettingsListener { ViewModel(), ICounterViewModel {
override var roundScoreList by mutableStateOf(emptyList<Round>()) override var roundScoreList by mutableStateOf(emptyList<Round>())
private set private set
override var totalScoreA by mutableStateOf(0) override var totalScoreA by mutableIntStateOf(0)
private set private set
override var totalScoreB by mutableStateOf(0) override var totalScoreB by mutableIntStateOf(0)
private set private set
override var teamNameA by mutableStateOf("") override var teamNameA by mutableStateOf("")
@@ -118,18 +118,9 @@ class CounterViewModel @Inject constructor(
override var teamNameSuggestionsB by mutableStateOf(listOf<String>()) override var teamNameSuggestionsB by mutableStateOf(listOf<String>())
private set private set
override var showVictoryDialog by mutableStateOf(false)
override var counterMode by mutableStateOf(CounterMode.LIST)
private set private set
init {
settingsAdapter.registerOnChangeListener(this)
}
override fun onCleared() {
settingsAdapter.unregisterOnChangeListener(this)
}
override var activeValue: String override var activeValue: String
get() { get() {
return if (isBFocused) { return if (isBFocused) {
@@ -171,9 +162,18 @@ class CounterViewModel @Inject constructor(
private var distinctTeamNames = listOf<String>() private var distinctTeamNames = listOf<String>()
private var lastGame: Game? = null
private var lastVictoryPoints: Int? = null
private val gameWon: Boolean
get() = totalScoreA >= settings.victoryPoints.value || totalScoreB >= settings.victoryPoints.value
private var lastRoundCount: Int = 0
init { init {
viewModelScope.launch { viewModelScope.launch {
gameRepository.getActiveGameFlow().collect { gameRepository.getActiveGameFlow().collect {
if (it != null) {
val score = it.getTotalPoints() val score = it.getTotalPoints()
roundScoreList = it.rounds roundScoreList = it.rounds
@@ -184,6 +184,32 @@ class CounterViewModel @Inject constructor(
teamNameB = it.game.nameB teamNameB = it.game.nameB
buildTeamNameSuggestions() buildTeamNameSuggestions()
// Game has changed
if (it.game.uid != lastGame?.uid) {
if (lastGame != null) {
settings.gameFinished.value = false
}
lastGame = it.game
lastRoundCount = it.rounds.size
return@collect
}
// 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
}
} }
} }
@@ -194,6 +220,17 @@ class CounterViewModel @Inject constructor(
buildTeamNameSuggestions() buildTeamNameSuggestions()
} }
} }
viewModelScope.launch {
settings.victoryPoints.collect {
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
}
}
} }
override fun focusLastInput() { override fun focusLastInput() {
@@ -302,12 +339,15 @@ class CounterViewModel @Inject constructor(
} }
} }
override fun changeCounterMode() { override fun victoryDialogExecuted(result: Boolean) {
val nextMode = when (settingsAdapter.counterMode) { showVictoryDialog = false
CounterMode.LIST -> CounterMode.GRAPH settings.gameFinished.value = true
CounterMode.GRAPH -> CounterMode.LIST
if (result) {
viewModelScope.launch {
gameRepository.newGame()
}
} }
settingsAdapter.setCounterMode(nextMode)
} }
override fun updateFocusStateA(state: Boolean) { override fun updateFocusStateA(state: Boolean) {
@@ -383,8 +423,4 @@ class CounterViewModel @Inject constructor(
return filtered.sorted().sortedBy { it.length }.take(10) return filtered.sorted().sortedBy { it.length }.take(10)
} }
override fun onCounterModeChanged(counterMode: CounterMode) {
this.counterMode = counterMode
}
} }

View File

@@ -19,12 +19,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Backspace import androidx.compose.material.icons.automirrored.outlined.Backspace
import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.KeyboardHide import androidx.compose.material.icons.outlined.KeyboardHide
import androidx.compose.material.icons.outlined.SwapHoriz import androidx.compose.material.icons.outlined.SwapHoriz
import androidx.compose.material3.Divider
import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
@@ -33,6 +33,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.focus.onFocusChanged
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -196,7 +198,7 @@ fun KeyboardView(
deleteButtonPressedState(deletePressed) deleteButtonPressedState(deletePressed)
KeyboardIconButton( KeyboardIconButton(
icon = Icons.Outlined.Backspace, icon = Icons.AutoMirrored.Outlined.Backspace,
interactionSource = interactionSource interactionSource = interactionSource
) {} ) {}
} }
@@ -290,6 +292,7 @@ fun CenteredTextField(
} }
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
CompositionLocalProvider(LocalTextInputService provides null) {
TextField( TextField(
value = value, value = value,
onValueChange = { }, onValueChange = { },
@@ -311,21 +314,23 @@ fun CenteredTextField(
onFocusStateChanged(it) onFocusStateChanged(it)
} }
) )
}
if (focused) { if (focused) {
val cursorColor = MaterialTheme.colorScheme.onSurface val cursorColor = MaterialTheme.colorScheme.onSurface
val infiniteTransition = rememberInfiniteTransition() val infiniteTransition = rememberInfiniteTransition(label = "blinkingCursor")
val alpha by infiniteTransition.animateFloat( val alpha by infiniteTransition.animateFloat(
0f, 0f,
cursorColor.alpha, cursorColor.alpha,
animationSpec = infiniteRepeatable( animationSpec = infiniteRepeatable(
animation = tween(500), animation = tween(500),
repeatMode = RepeatMode.Reverse repeatMode = RepeatMode.Reverse
) ), label = "blinkingCursor"
) )
Row { Row {
Text(text = value, color = cursorColor.copy(alpha = 0f)) Text(text = value, color = cursorColor.copy(alpha = 0f))
Divider( HorizontalDivider(
modifier = Modifier modifier = Modifier
.padding(start = 3.dp, top = 15.dp, bottom = 15.dp) .padding(start = 3.dp, top = 15.dp, bottom = 15.dp)
.width(1.dp) .width(1.dp)

View File

@@ -1,104 +0,0 @@
package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.tooling.preview.Preview
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.core.axis.formatter.DecimalFormatAxisValueFormatter
import com.patrykandpatrick.vico.core.chart.line.LineChart
import com.patrykandpatrick.vico.core.chart.values.AxisValuesOverrider
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer.Companion.composedChartEntryModelOf
import com.patrykandpatrick.vico.core.entry.entryModelOf
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.ui.AppTheme
import kotlin.math.ceil
import kotlin.math.floor
@Composable
fun RoundGraphView(rounds: List<Round>, modifier: Modifier = Modifier) {
val points = getPoints(rounds)
val range = getRange(points.first, points.second, 50)
val specA = LineChart.LineSpec(MaterialTheme.colorScheme.primary.toArgb())
val specB = LineChart.LineSpec(MaterialTheme.colorScheme.secondary.toArgb())
Chart(
chart = lineChart(
lines = listOf(specA, specB),
axisValuesOverrider = AxisValuesOverrider.fixed(
minY = range.first,
maxY = range.second
)
),
model = composedChartEntryModelOf(
listOf(
entryModelOf(points.first),
entryModelOf(points.second)
)
),
startAxis = startAxis(
maxLabelCount = 10,
valueFormatter = DecimalFormatAxisValueFormatter("#")
),
chartScrollSpec = rememberChartScrollSpec(isScrollEnabled = false),
modifier = modifier
)
}
private fun getPoints(rounds: List<Round>): Pair<List<FloatEntry>, List<FloatEntry>> {
val a = mutableListOf(FloatEntry(0f, 0f))
val b = mutableListOf(FloatEntry(0f, 0f))
var sumA = 0
var sumB = 0
rounds.forEachIndexed { index, round ->
sumA += round.scoreA
sumB += round.scoreB
a.add(FloatEntry((index + 1).toFloat(), sumA.toFloat()))
b.add(FloatEntry((index + 1).toFloat(), sumB.toFloat()))
}
return Pair(a, b)
}
private fun getRange(a: List<FloatEntry>, b: List<FloatEntry>, step: Int): Pair<Float, Float> {
val min = (a + b).minBy { it.y }
val max = (a + b).maxBy { it.y }
val lo = floor(min.y / step) * step
val hi = ceil(max.y / step) * step
return Pair(lo, hi)
}
@Preview(name = "Light Mode")
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
fun RoundGraphViewPreview() {
val rounds = listOf(
Round(1, 10, -10),
Round(1, 5, 95),
Round(1, 100, 0),
Round(1, 125, -25),
Round(1, 50, 50)
)
AppTheme {
Surface {
RoundGraphView(rounds, Modifier)
}
}
}

View File

@@ -21,7 +21,7 @@ import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.ui.AppTheme import me.zobrist.tichucounter.ui.AppTheme
@Composable @Composable
fun RoundListView(rounds: List<Round>, modifier: Modifier = Modifier) { fun RoundListView(rounds: List<Round>, modifier: Modifier) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()

View File

@@ -1,44 +1,64 @@
package me.zobrist.tichucounter.ui.history 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.ExperimentalFoundationApi
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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons 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.DeleteForever
import androidx.compose.material.icons.outlined.MoreVert import androidx.compose.material.icons.outlined.RestartAlt
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Badge
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.R import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.getTotalPoints import me.zobrist.tichucounter.domain.getTotalPoints
import me.zobrist.tichucounter.ui.composables.DropDownMenu
import java.text.DateFormat import java.text.DateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -47,11 +67,14 @@ import java.util.Locale
@Composable @Composable
fun HistoryList( fun HistoryList(
viewModel: HistoryViewModel, viewModel: HistoryViewModel,
navigateToCalculator: () -> Unit snackbarHostState: SnackbarHostState,
navigateToCalculator: () -> Unit,
) { ) {
val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState()
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
DeleteConfirmDialog(showDeleteDialog) { DeleteConfirmDialog(showDeleteDialog) {
showDeleteDialog = false showDeleteDialog = false
if (it) { if (it) {
@@ -59,14 +82,51 @@ 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( HistoryList(
viewModel.gameAndHistory, games = viewModel.gameAndHistory,
{ onOpenClicked = {
scope.launch {
viewModel.activateGame(it) viewModel.activateGame(it)
lazyListState.animateScrollToItem(0)
snackbarHostState.currentSnackbarData?.dismiss()
val result = snackbarHostState.showSnackbar(
message = activatedMessage,
actionLabel = activatedActionLabel,
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
navigateToCalculator() navigateToCalculator()
}
}
}, },
{ viewModel.deleteGame(it) }, onDeleteClicked = {
{ showDeleteDialog = true }, scope.launch {
viewModel.deleteGame(it)
snackbarHostState.currentSnackbarData?.dismiss()
val result = snackbarHostState.showSnackbar(
message = deletedMessage,
actionLabel = deletedActionLabel,
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.Dismissed) {
viewModel.deleteGame(it)
} else {
viewModel.restoreLastDeletedGame()
}
}
},
onDeleteAllClicked = { showDeleteDialog = true },
lazyListState = lazyListState
) )
} }
@@ -78,14 +138,12 @@ fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {}
AlertDialog( AlertDialog(
onDismissRequest = { onExecuted(false) }, onDismissRequest = { onExecuted(false) },
dismissButton = { dismissButton = {
TextButton({ onExecuted(false) }) TextButton({ onExecuted(false) }) {
{
Text(stringResource(R.string.cancel)) Text(stringResource(R.string.cancel))
} }
}, },
confirmButton = { confirmButton = {
TextButton({ onExecuted(true) }) TextButton({ onExecuted(true) }) {
{
Text(stringResource(R.string.ok)) Text(stringResource(R.string.ok))
} }
}, },
@@ -95,47 +153,44 @@ fun DeleteConfirmDialog(show: Boolean = true, onExecuted: (Boolean) -> Unit = {}
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun HistoryList( fun HistoryList(
games: List<GameWithScores>, games: List<GameWithScores>,
onOpenClicked: (GameId: Long) -> Unit, onOpenClicked: (gameId: Long) -> Unit,
onDeleteClicked: (GameId: Long) -> Unit, onDeleteClicked: (gameId: Long) -> Unit,
onDeleteAllClicked: () -> Unit onDeleteAllClicked: () -> Unit,
lazyListState: LazyListState = LazyListState(),
) { ) {
Row { Row {
LazyColumn { LazyColumn(state = lazyListState) {
item { items(
Text( items = games,
modifier = Modifier.padding(start = 10.dp, end = 10.dp), key = { it.hashCode() }) {
text = stringResource(R.string.active), if (it.game.active) {
style = MaterialTheme.typography.headlineSmall HistoryListItem(
it,
Modifier
.animateItemPlacement()
.padding(2.dp)
)
} else {
DismissibleHistoryListItem(
it,
Modifier.animateItemPlacement(),
onOpenClicked,
onDeleteClicked
) )
} }
items(games.filter { it.game.active }) {
HistoryListItem(it, onOpenClicked, onDeleteClicked)
}
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(games.filter { !it.game.active }) {
HistoryListItem(it, onOpenClicked, onDeleteClicked)
} }
item { item {
Button( Button(enabled = games.count() > 1,
enabled = games.count() > 1,
modifier = Modifier modifier = Modifier
.padding(start = 4.dp, end = 4.dp, top = 10.dp) .padding(start = 4.dp, end = 4.dp, top = 10.dp)
.align(CenterVertically) .align(CenterVertically)
.fillMaxWidth(), .fillMaxWidth()
.animateItemPlacement(),
onClick = { onDeleteAllClicked() }) { onClick = { onDeleteAllClicked() }) {
Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null) Icon(imageVector = Icons.Outlined.DeleteForever, contentDescription = null)
Text(text = stringResource(id = R.string.deleteAll)) Text(text = stringResource(id = R.string.deleteAll))
@@ -143,40 +198,146 @@ fun HistoryList(
} }
} }
} }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DismissibleHistoryListItem(
game: GameWithScores,
modifier: Modifier = Modifier,
onOpenClicked: (gameId: Long) -> Unit,
onDeleteClicked: (gameId: Long) -> Unit,
) {
val density = LocalDensity.current
val dismissState =
rememberSwipeToDismissBoxState(positionalThreshold = { with(density) { 100.dp.toPx() } },
confirmValueChange = {
if (it == SwipeToDismissBoxValue.EndToStart) {
onDeleteClicked(game.game.uid)
}
if (it == SwipeToDismissBoxValue.StartToEnd) {
onOpenClicked(game.game.uid)
}
true
})
SwipeToDismissBox(
modifier = modifier,
state = dismissState,
enableDismissFromEndToStart = true,
enableDismissFromStartToEnd = true,
backgroundContent = {
ItemBackground(dismissState.targetValue)
}, content = {
HistoryListItem(game = game, modifier = Modifier.padding(2.dp))
})
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemBackground(
dismissBoxValue: SwipeToDismissBoxValue
) {
val backgroundColor by animateColorAsState(
when (dismissBoxValue) {
SwipeToDismissBoxValue.EndToStart -> MaterialTheme.colorScheme.error
SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.primary
else -> MaterialTheme.colorScheme.background
}, label = ""
)
val textColor by animateColorAsState(
when (dismissBoxValue) {
SwipeToDismissBoxValue.EndToStart -> MaterialTheme.colorScheme.onError
SwipeToDismissBoxValue.StartToEnd -> MaterialTheme.colorScheme.onPrimary
else -> MaterialTheme.colorScheme.onBackground
}, label = ""
)
val scale by animateFloatAsState(
if (dismissBoxValue == SwipeToDismissBoxValue.Settled) 0.75f else 1f, label = ""
)
Box(
Modifier
.fillMaxSize()
.padding(top = 2.dp, bottom = 2.dp)
.background(backgroundColor)
.padding(horizontal = 10.dp),
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.CenterStart)
) {
Icon(
Icons.Outlined.RestartAlt,
contentDescription = null,
tint = textColor,
modifier = Modifier.scale(scale),
)
Text(text = stringResource(id = R.string.continue_play), color = textColor)
}
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.CenterEnd)
) {
Icon(
Icons.Outlined.Delete,
contentDescription = null,
tint = textColor,
modifier = Modifier.scale(scale),
)
Text(text = stringResource(id = R.string.delete), color = textColor)
}
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun BackgroundPreview() {
Column {
Box(modifier = Modifier.height(60.dp))
{
ItemBackground(SwipeToDismissBoxValue.Settled)
}
Box(modifier = Modifier.height(60.dp))
{
ItemBackground(SwipeToDismissBoxValue.EndToStart)
}
Box(modifier = Modifier.height(60.dp))
{
ItemBackground(SwipeToDismissBoxValue.StartToEnd)
}
}
}
@Composable @Composable
fun HistoryListItem( fun HistoryListItem(
game: GameWithScores, game: GameWithScores, modifier: Modifier = Modifier
onOpenClicked: (GameId: Long) -> Unit,
onDeleteClicked: (GameId: Long) -> Unit
) { ) {
val format = val format =
DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()) 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() val totalScores = game.getTotalPoints()
Card( Card(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(all = 4.dp)
.clickable { onOpenClicked(game.game.uid) },
colors = cardColor
) { ) {
Row( Row(
Modifier Modifier.padding(all = 12.dp)
.padding(all = 12.dp)
) { ) {
Column(Modifier.weight(4f)) { Box(modifier = modifier.fillMaxSize()) {
Column {
Text( Text(
text = game.game.nameA + " vs " + game.game.nameB, text = game.game.nameA + " vs " + game.game.nameB,
maxLines = 1, maxLines = 1,
@@ -193,35 +354,16 @@ fun HistoryListItem(
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall
) )
} }
Column( if (game.game.active) {
Modifier Badge(
.wrapContentSize() modifier = Modifier.align(TopEnd),
.width(40.dp) contentColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.primary
) { ) {
Text(
if (!game.game.active) { text = stringResource(id = R.string.active),
var expanded by remember { mutableStateOf(false) } style = MaterialTheme.typography.labelSmall
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,25 +376,17 @@ fun HistoryListItem(
private fun HistoryListPreview() { private fun HistoryListPreview() {
val tempData = listOf( val tempData = listOf(
GameWithScores( GameWithScores(
Game(true, "abc", "def", Date(), Date()), Game(true, "abcsdf sdaf asdf sdf ", "defsadf asdf sadf ", Date(), Date()),
listOf(Round(1, 550, 500)) listOf(Round(1, 550, 500))
), ), GameWithScores(
GameWithScores( Game(false, "ADTH", "dogfg", Date(), Date()), listOf(Round(2, 20, 60))
Game(false, "ADTH", "dogfg", Date(), Date()), ), GameWithScores(
listOf(Round(2, 20, 60)) Game(false, "TeamA3 langer Name", "TeamB3", Date(), Date()), listOf(Round(3, 30, 70))
), ), GameWithScores(
GameWithScores( Game(false, "TeamA4", "TeamB4", Date(), Date()), listOf(Round(4, 40, 80))
Game(false, "TeamA3 langer Name", "TeamB3", Date(), Date()), ), GameWithScores(
listOf(Round(3, 30, 70)) Game(false, "TeamA5", "TeamB5", Date(), Date()), listOf(Round(5, 50, 90))
),
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, {}, {}) {} HistoryList(tempData, {}, {}, {})
} }

View File

@@ -20,12 +20,15 @@ class HistoryViewModel @Inject constructor(
var gameAndHistory by mutableStateOf(emptyList<GameWithScores>()) var gameAndHistory by mutableStateOf(emptyList<GameWithScores>())
private set private set
private var fullList: List<GameWithScores> = emptyList()
init { init {
viewModelScope.launch { viewModelScope.launch {
gameRepository.getAllWithRoundFlow().collect { games -> gameRepository.getAllWithRoundFlow().collect { games ->
gameAndHistory = fullList =
games.sortedBy { it.game.modified }.sortedBy { it.game.active }.reversed() games.sortedBy { it.game.modified }.sortedBy { it.game.active }.reversed()
gameAndHistory = fullList
} }
} }
} }
@@ -36,6 +39,12 @@ class HistoryViewModel @Inject constructor(
} }
} }
fun restoreLastDeletedGame() {
viewModelScope.launch {
gameRepository.restoreLastDeletedGame()
}
}
fun activateGame(gameId: Long) { fun activateGame(gameId: Long) {
viewModelScope.launch { viewModelScope.launch {
gameRepository.setActive(gameId) gameRepository.setActive(gameId)

View File

@@ -3,10 +3,10 @@ package me.zobrist.tichucounter.ui.layout
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.List
import androidx.compose.material.icons.outlined.Calculate import androidx.compose.material.icons.outlined.Calculate
import androidx.compose.material.icons.outlined.List
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet import androidx.compose.material3.ModalDrawerSheet
@@ -38,7 +38,7 @@ fun DrawerContent(
text = stringResource(R.string.app_name), text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall
) )
Divider(modifier = Modifier.padding(10.dp)) HorizontalDivider(modifier = Modifier.padding(10.dp))
drawerItems.forEach { screen -> drawerItems.forEach { screen ->
NavigationDrawerItem( NavigationDrawerItem(
@@ -58,7 +58,7 @@ fun DrawerContent(
fun DrawerContentPreview() { fun DrawerContentPreview() {
val counter = DrawerItem(Route.COUNTER, Icons.Outlined.Calculate, "Counter") val counter = DrawerItem(Route.COUNTER, Icons.Outlined.Calculate, "Counter")
val history = DrawerItem(Route.HISTORY, Icons.Outlined.List, "History") val history = DrawerItem(Route.HISTORY, Icons.AutoMirrored.Outlined.List, "History")
val settings = DrawerItem(Route.SETTINGS, Icons.Outlined.Settings, "Settings") val settings = DrawerItem(Route.SETTINGS, Icons.Outlined.Settings, "Settings")
AppTheme { AppTheme {
Surface { Surface {

View File

@@ -1,6 +1,10 @@
package me.zobrist.tichucounter.ui.layout package me.zobrist.tichucounter.ui.layout
import androidx.compose.material3.* import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow

View File

@@ -44,6 +44,7 @@ val themeMap = mapOf(
Theme.LIGHT to R.string.light Theme.LIGHT to R.string.light
) )
val victoryPointsList = listOf(500, 1000, 1500, 2000)
@Composable @Composable
fun SettingsView(viewModel: SettingsViewModel) { fun SettingsView(viewModel: SettingsViewModel) {
@@ -51,9 +52,11 @@ fun SettingsView(viewModel: SettingsViewModel) {
viewModel.screenOn.value, viewModel.screenOn.value,
viewModel.language, viewModel.language,
viewModel.theme, viewModel.theme,
viewModel.victoryPoints,
{ viewModel.updateScreenOn(it) }, { viewModel.updateScreenOn(it) },
{ viewModel.updateLanguage(it) }, { viewModel.updateLanguage(it) },
{ viewModel.updateTheme(it) }) { viewModel.updateTheme(it) },
{ viewModel.updateVictoryPoints(it) })
} }
@Composable @Composable
@@ -61,11 +64,22 @@ fun SettingsView(
valueScreenOn: Boolean = true, valueScreenOn: Boolean = true,
valueLanguage: Language = Language.ENGLISH, valueLanguage: Language = Language.ENGLISH,
valueTheme: Theme = Theme.DARK, valueTheme: Theme = Theme.DARK,
valueVictoryPoints: Int = 1000,
updateScreenOn: (KeepScreenOn) -> Unit = {}, updateScreenOn: (KeepScreenOn) -> Unit = {},
updateLanguage: (Language) -> 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( BooleanSetting(
stringResource(R.string.keep_screen_on), stringResource(R.string.keep_screen_on),
valueScreenOn valueScreenOn
@@ -82,6 +96,18 @@ fun SettingsView(
themeMap, themeMap,
valueTheme, valueTheme,
) { updateTheme(it) } ) { 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( Row(
Modifier Modifier
.padding(20.dp) .padding(bottom = 15.dp, top = 5.dp)
.fillMaxWidth() .fillMaxWidth()
) { ) {
Column(Modifier.weight(5f)) { Column(Modifier.weight(5f)) {
@@ -119,22 +145,33 @@ fun BooleanSetting(name: String, value: Boolean, updateValue: (Boolean) -> Unit)
@Composable @Composable
fun <T> StringSetting(name: String, map: Map<T, Int>, selected: T, onSelected: (T) -> Unit) { fun <T> StringSetting(name: String, map: Map<T, Int>, 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 <T> ListSetting(name: String, list: Collection<T>, selected: T, onSelected: (T) -> Unit) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Row( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(20.dp) .padding(bottom = 15.dp, top = 5.dp)
.clickable { expanded = true }) { .clickable { expanded = true }) {
Column(Modifier.weight(5f)) { Column(Modifier.weight(5f)) {
Text(name, style = MaterialTheme.typography.bodyLarge, overflow = TextOverflow.Ellipsis) Text(name, style = MaterialTheme.typography.bodyLarge, overflow = TextOverflow.Ellipsis)
map[selected]?.let {
Text( Text(
stringResource(it), selected.toString(),
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.labelLarge
) )
} }
}
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Icon( Icon(
@@ -142,10 +179,9 @@ fun <T> StringSetting(name: String, map: Map<T, Int>, selected: T, onSelected: (
contentDescription = null, contentDescription = null,
modifier = Modifier.align(End) modifier = Modifier.align(End)
) )
}
DropDownMenu( DropDownMenu(
map, list,
selected, selected,
expanded, expanded,
) { ) {
@@ -153,6 +189,7 @@ fun <T> StringSetting(name: String, map: Map<T, Int>, selected: T, onSelected: (
it?.let { onSelected(it) } 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,
) {}
}
}
}

View File

@@ -1,6 +1,7 @@
package me.zobrist.tichucounter.ui.settings package me.zobrist.tichucounter.ui.settings
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -15,28 +16,35 @@ import javax.inject.Inject
class SettingsViewModel @Inject constructor(private val settings: SettingsAdapter) : ViewModel() { class SettingsViewModel @Inject constructor(private val settings: SettingsAdapter) : ViewModel() {
var language by mutableStateOf(settings.language) var language by mutableStateOf(settings.language.value)
private set private set
var theme by mutableStateOf(settings.theme) var theme by mutableStateOf(settings.theme.value)
private set private set
var screenOn by mutableStateOf(settings.keepScreenOn) var screenOn by mutableStateOf(settings.keepScreenOn.value)
private set
var victoryPoints by mutableIntStateOf(settings.victoryPoints.value)
private set private set
fun updateLanguage(language: Language) { fun updateLanguage(language: Language) {
settings.setLanguage(language) settings.language.value = language
this.language = settings.language this.language = language
} }
fun updateTheme(theme: Theme) { fun updateTheme(theme: Theme) {
settings.setTheme(theme) settings.theme.value = theme
this.theme = settings.theme this.theme = theme
} }
fun updateScreenOn(value: KeepScreenOn) { fun updateScreenOn(value: KeepScreenOn) {
settings.setKeepScreenOn(value) settings.keepScreenOn.value = value
screenOn = settings.keepScreenOn screenOn = value
} }
fun updateVictoryPoints(value: Int) {
settings.victoryPoints.value = value
victoryPoints = value
}
} }

View File

@@ -12,7 +12,7 @@
<string name="menu_settings">Einstellungen</string> <string name="menu_settings">Einstellungen</string>
<string name="on">Ein</string> <string name="on">Ein</string>
<string name="off">Aus</string> <string name="off">Aus</string>
<string name="newGame">Neues Spiel</string> <string name="new_game">Neues Spiel</string>
<string name="delete_inactive_title">Verlauf löschen</string> <string name="delete_inactive_title">Verlauf löschen</string>
<string name="delete_inactive_text">Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden.</string> <string name="delete_inactive_text">Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="cancel">Abbrechen</string> <string name="cancel">Abbrechen</string>
@@ -24,5 +24,17 @@
<string name="menu_counter">Counter</string> <string name="menu_counter">Counter</string>
<string name="menu_about">About</string> <string name="menu_about">About</string>
<string name="contact_us">Schreib uns</string> <string name="contact_us">Schreib uns</string>
<string name="continue_play">Weiterspielen</string>
<string name="delete_success">Spiel gelöscht.</string>
<string name="undo_question">RÜCKGÄNGIG</string>
<string name="activated_success">Spiel aktiviert.</string>
<string name="to_calculator_question">WEITERSPIELEN</string>
<string name="display">Anzeige</string>
<string name="game">Spiel</string>
<string name="victory_points">Siegespunkte</string>
<string name="victory_title">%1$s hat gewonnen</string>
<string name="draw_message">Sieht aus, als ob ihr ein neues Spiel starten solltet, um das endgültig zu klären.</string>
<string name="draw_title">Unentschieden</string>
<string name="victory_message">Herzliche Gratulation! Wie wäre es mit einer Revanche?</string>
<string name="abort">Abbrechen</string>
</resources> </resources>

View File

@@ -15,9 +15,9 @@
<string name="menu_settings">Settings</string> <string name="menu_settings">Settings</string>
<string name="on">On</string> <string name="on">On</string>
<string name="off">Off</string> <string name="off">Off</string>
<string name="newGame">New Game</string> <string name="new_game">New Game</string>
<string name="delete_inactive_title">Delete history</string> <string name="delete_inactive_title">Delete history</string>
<string name="delete_inactive_text">You really want to delete the the history? This action can\'t be undone.</string> <string name="delete_inactive_text">You really want to delete the history? This action can\'t be undone.</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
@@ -28,4 +28,17 @@
<string name="menu_about">About</string> <string name="menu_about">About</string>
<string name="contact_us">Contact us</string> <string name="contact_us">Contact us</string>
<string name="play_store" translatable="false">Play Store</string> <string name="play_store" translatable="false">Play Store</string>
<string name="continue_play">Continue game</string>
<string name="delete_success">Game deleted.</string>
<string name="undo_question">UNDO</string>
<string name="activated_success">Game activated.</string>
<string name="to_calculator_question">CONTINUE PLAYING</string>
<string name="display">Display</string>
<string name="game">Game</string>
<string name="victory_points">Victory points</string>
<string name="victory_title">%1$s won the game</string>
<string name="draw_message">Looks like you should start a new game to settle this for good.</string>
<string name="draw_title">Draw</string>
<string name="victory_message">Congratulations! How about a rematch?</string>
<string name="abort">Abort</string>
</resources> </resources>

View File

@@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.8.21" ext.kotlin_version = "1.9.24"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:8.0.2' classpath 'com.android.tools.build:gradle:8.5.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
@@ -15,8 +15,9 @@ buildscript {
} }
plugins { plugins {
id 'com.google.dagger.hilt.android' version '2.44' apply false id 'com.google.dagger.hilt.android' version '2.51.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
id 'com.google.devtools.ksp' version '1.9.24-1.0.20' apply false
} }
allprojects { allprojects {
@@ -25,7 +26,3 @@ allprojects {
mavenCentral() mavenCentral()
} }
} }
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip