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
This commit was merged in pull request #50.
This commit is contained in:
2023-09-01 18:30:29 +02:00
11 changed files with 260 additions and 86 deletions

View File

@@ -40,12 +40,11 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.android.play.core.review.ReviewManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.domain.DrawerItem
import me.zobrist.tichucounter.domain.ISettingsChangeListener
import me.zobrist.tichucounter.domain.ISystemSettingsChangeListener
import me.zobrist.tichucounter.domain.KeepScreenOn
import me.zobrist.tichucounter.domain.Language
import me.zobrist.tichucounter.domain.ReviewService
@@ -70,7 +69,7 @@ import me.zobrist.tichucounter.ui.settings.SettingsViewModel
import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), ISettingsChangeListener {
class MainActivity : AppCompatActivity(), ISystemSettingsChangeListener {
@Inject
lateinit var settingsAdapter: SettingsAdapter
@@ -192,7 +191,6 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
var topBarState by remember { mutableStateOf(TopBarState()) }
var snackbarHostState by remember { mutableStateOf(SnackbarHostState()) }
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
@@ -211,7 +209,7 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
startDestination = Route.COUNTER.name,
modifier = Modifier.padding(paddings)
) {
composable(Route.COUNTER.name) {
this.composable(Route.COUNTER.name.toString()) {
var expanded by remember { mutableStateOf(false) }
@@ -231,15 +229,16 @@ class MainActivity : AppCompatActivity(), ISettingsChangeListener {
mainViewModel.activeGameHasRounds,
{ expanded = true }
) {
val newGameTranslated = stringResource(R.string.new_game)
DropDownMenu(
mapOf("new" to R.string.newGame),
listOf(newGameTranslated),
"",
expanded,
) {
expanded = false
it?.let {
when (it) {
"new" -> mainViewModel.newGame()
newGameTranslated -> mainViewModel.newGame()
}
}
}

View File

@@ -2,27 +2,17 @@ package me.zobrist.tichucounter.domain
import android.app.Activity
import android.content.Context
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import com.google.android.play.core.review.ReviewManagerFactory
import com.google.android.play.core.review.testing.FakeReviewManager
import dagger.hilt.android.internal.Contexts
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import dagger.hilt.android.scopes.FragmentScoped
import dagger.hilt.android.scopes.ViewScoped
import java.time.Duration
import java.time.Period
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
class ReviewService @Inject constructor(@ActivityContext private val appContext: Context) {
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(appContext)
private val THREE_MONTHS : Long = 7776000000
private val THREE_MONTHS: Long = 7776000000
private var requestCalled: Int
get() = sharedPreferences.getInt("requestCalled", 0)
@@ -43,10 +33,8 @@ class ReviewService @Inject constructor(@ActivityContext private val appContext:
fun request() {
requestCalled += 1
if(requestCalled >= 3)
{
if(nextReviewedDate.time < System.currentTimeMillis())
{
if (requestCalled >= 3) {
if (nextReviewedDate.time < System.currentTimeMillis()) {
requestCalled = 0
nextReviewedDate = Date(System.currentTimeMillis() + THREE_MONTHS)

View File

@@ -4,7 +4,6 @@ import android.content.Context
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
@@ -17,12 +16,19 @@ enum class Language(val value: LocaleListCompat) {
enum class KeepScreenOn(val value: Boolean) { ON(true), OFF(false) }
interface ISettingsChangeListener {
typealias VictoryPoints = Int
interface ISettingsChangeListener
interface ISystemSettingsChangeListener : ISettingsChangeListener {
fun onLanguageChanged(language: Language)
fun onThemeChanged(theme: Theme)
fun onScreenOnChanged(keepOn: KeepScreenOn)
}
interface IGameSettingsChangeListener : ISettingsChangeListener {
fun onVictoryPointsChanged(victoryPoints: Int)
}
@Singleton
class SettingsAdapter @Inject constructor(@ApplicationContext private val context: Context) {
@@ -37,9 +43,9 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex
var keepScreenOn: KeepScreenOn
private set
var reviewDialogShownDate: Date
get() = Date(sharedPreferences.getLong("reviewDialogShownDate", 0))
set(value) = updatePreference("reviewDialogShownDate", value.time)
var victoryPoints: Int
private set
init {
language = try {
@@ -59,16 +65,24 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex
} catch (_: java.lang.Exception) {
KeepScreenOn.OFF
}
victoryPoints = sharedPreferences.getInt(VictoryPoints::class.simpleName, 1000)
}
fun registerOnChangeListener(listener: ISettingsChangeListener) {
listenerList.add(listener)
if (listener is ISystemSettingsChangeListener) {
listener.onThemeChanged(theme)
listener.onLanguageChanged(language)
listener.onScreenOnChanged(keepScreenOn)
}
if (listener is IGameSettingsChangeListener) {
listener.onVictoryPointsChanged(victoryPoints)
}
}
fun unregisterOnChangeListener(listener: ISettingsChangeListener?) {
if (listener != null) {
listenerList.remove(listener)
@@ -93,6 +107,12 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex
notifyListeners(setting)
}
fun setVictoryPoints(setting: Int) {
this.victoryPoints = setting
updatePreference(VictoryPoints::class.simpleName, setting)
notifyListeners(setting)
}
private fun updatePreference(name: String?, value: String) {
val editor = sharedPreferences.edit()
editor.putString(name, value)
@@ -105,22 +125,33 @@ class SettingsAdapter @Inject constructor(@ApplicationContext private val contex
editor.apply()
}
private fun updatePreference(name: String?, value: Int) {
val editor = sharedPreferences.edit()
editor.putInt(name, value)
editor.apply()
}
private fun notifyListeners(language: Language) {
listenerList.forEach {
listenerList.filterIsInstance<ISystemSettingsChangeListener>().forEach {
it.onLanguageChanged(language)
}
}
private fun notifyListeners(theme: Theme) {
listenerList.forEach {
listenerList.filterIsInstance<ISystemSettingsChangeListener>().forEach {
it.onThemeChanged(theme)
}
}
private fun notifyListeners(keepScreenOn: KeepScreenOn) {
listenerList.forEach {
it.onScreenOnChanged(keepScreenOn)
private fun notifyListeners(victoryPoints: VictoryPoints) {
listenerList.filterIsInstance<IGameSettingsChangeListener>().forEach {
it.onVictoryPointsChanged(victoryPoints)
}
}
private fun notifyListeners(keepScreenOn: KeepScreenOn) {
listenerList.filterIsInstance<ISystemSettingsChangeListener>().forEach {
it.onScreenOnChanged(keepScreenOn)
}
}
}

View File

@@ -7,10 +7,8 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.ReviewService
import me.zobrist.tichucounter.repository.GameRepository
import javax.inject.Inject

View File

@@ -7,25 +7,29 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
@Composable
fun <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(
expanded = expanded,
onDismissRequest = { onSelected(null) }
) {
map.forEach {
list.forEach {
DropdownMenuItem(
onClick = {
onSelected(it.key)
onSelected(it)
},
trailingIcon = {
if (it.key == selected) {
if (it == selected) {
Icon(Icons.Outlined.Check, null)
}
},
text = { Text(stringResource(it.value)) },
text = { Text(it.toString()) },
)
}
}

View File

@@ -3,7 +3,13 @@ package me.zobrist.tichucounter.ui.counter
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.EmojiEvents
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -12,7 +18,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import me.zobrist.tichucounter.R
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.ui.AppTheme
@@ -23,6 +31,18 @@ fun Counter(viewModel: ICounterViewModel = PreviewViewModel()) {
var orientation by remember { mutableStateOf(Configuration.ORIENTATION_PORTRAIT) }
orientation = LocalConfiguration.current.orientation
if (viewModel.showVictoryDialog) {
GameVictoryDialog(
viewModel.totalScoreA,
viewModel.totalScoreB,
viewModel.teamNameA,
viewModel.teamNameB,
{ viewModel.victoryDialogExecuted(false) })
{
viewModel.victoryDialogExecuted(true)
}
}
Surface {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
Landscape(viewModel)
@@ -102,6 +122,54 @@ fun CounterViewPreview() {
}
}
@Preview()
@Composable
fun GameVictoryDialog(
pointsA: Int = 2000,
pointsB: Int = 50,
nameA: String = "nameA",
nameB: String = "nameB",
onDismiss: () -> Unit = {},
onNewGame: () -> Unit = {},
) {
val winner = if (pointsA > pointsB) {
nameA
} else {
nameB
}
val message = if (pointsA == pointsB) {
stringResource(R.string.draw_message, winner, 100)
} else {
stringResource(R.string.victory_message)
}
val title = if (pointsA == pointsB) {
stringResource(R.string.draw_title)
} else {
stringResource(R.string.victory_title, winner)
}
AlertDialog(
onDismissRequest = { onDismiss() },
dismissButton = {
TextButton({ onDismiss() }) {
Text(stringResource(R.string.continue_play))
}
},
confirmButton = {
TextButton({ onNewGame() }) {
Text(stringResource(R.string.new_game))
}
},
icon = { Icon(Icons.Outlined.EmojiEvents, null) },
title = { Text(title) },
text = { Text(message) }
)
}
internal class PreviewViewModel : ICounterViewModel {
override var roundScoreList: List<Round> =
listOf(Round(1, 10, 90), Round(1, 50, 50), Round(1, 70, 30))
@@ -123,6 +191,7 @@ internal class PreviewViewModel : ICounterViewModel {
listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long")
override val teamNameSuggestionsB: List<String> =
listOf("TeamA", "asdffd", "TeamB", "really really long Team Name that is way too long")
override var showVictoryDialog: Boolean = false
override fun focusLastInput() {
}
@@ -155,6 +224,9 @@ internal class PreviewViewModel : ICounterViewModel {
override fun updateNameB(value: String) {
}
override fun victoryDialogExecuted(result: Boolean) {
}
override fun updateFocusStateA(state: Boolean) {
}

View File

@@ -11,7 +11,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round
import me.zobrist.tichucounter.domain.IGameSettingsChangeListener
import me.zobrist.tichucounter.domain.SettingsAdapter
import me.zobrist.tichucounter.domain.Tichu
import me.zobrist.tichucounter.domain.digitCount
import me.zobrist.tichucounter.domain.getTotalPoints
@@ -58,16 +61,19 @@ interface ICounterViewModel : IKeyBoardViewModel {
val teamNameB: String
val teamNameSuggestionsA: List<String>
val teamNameSuggestionsB: List<String>
val showVictoryDialog: Boolean
fun updateNameA(value: String)
fun updateNameB(value: String)
fun victoryDialogExecuted(result: Boolean)
}
@HiltViewModel
class CounterViewModel @Inject constructor(
private val gameRepository: GameRepository
private val gameRepository: GameRepository,
private val settings: SettingsAdapter
) :
ViewModel(), ICounterViewModel {
ViewModel(), ICounterViewModel, IGameSettingsChangeListener {
override var roundScoreList by mutableStateOf(emptyList<Round>())
private set
@@ -113,6 +119,8 @@ class CounterViewModel @Inject constructor(
override var teamNameSuggestionsB by mutableStateOf(listOf<String>())
private set
override var showVictoryDialog by mutableStateOf(false)
private set
override var activeValue: String
get() {
@@ -155,11 +163,14 @@ class CounterViewModel @Inject constructor(
private var distinctTeamNames = listOf<String>()
private var victoryDialogShown = false
private var lastGame: Game? = null
init {
viewModelScope.launch {
gameRepository.getActiveGameFlow().collect {
if (it != null) {
val score = it.getTotalPoints()
roundScoreList = it.rounds
@@ -171,6 +182,16 @@ class CounterViewModel @Inject constructor(
buildTeamNameSuggestions()
if (it.game.uid != lastGame?.uid) {
victoryDialogShown = false
lastGame = it.game
}
if (!victoryDialogShown) {
if (totalScoreA >= settings.victoryPoints || totalScoreB >= settings.victoryPoints) {
showVictoryDialog = true
}
}
}
}
}
@@ -182,6 +203,12 @@ class CounterViewModel @Inject constructor(
buildTeamNameSuggestions()
}
}
settings.registerOnChangeListener(this)
}
override fun onCleared() {
settings.unregisterOnChangeListener(this)
}
override fun focusLastInput() {
@@ -290,6 +317,17 @@ class CounterViewModel @Inject constructor(
}
}
override fun victoryDialogExecuted(result: Boolean) {
showVictoryDialog = false
victoryDialogShown = true
if (result) {
viewModelScope.launch {
gameRepository.newGame()
}
}
}
override fun updateFocusStateA(state: Boolean) {
isAFocused = state
if (state) {
@@ -363,4 +401,8 @@ class CounterViewModel @Inject constructor(
return filtered.sorted().sortedBy { it.length }.take(10)
}
override fun onVictoryPointsChanged(victoryPoints: Int) {
victoryDialogShown = false
}
}

View File

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

@@ -24,6 +24,9 @@ class SettingsViewModel @Inject constructor(private val settings: SettingsAdapte
var screenOn by mutableStateOf(settings.keepScreenOn)
private set
var victoryPoints by mutableStateOf(settings.victoryPoints)
private set
fun updateLanguage(language: Language) {
settings.setLanguage(language)
this.language = settings.language
@@ -39,4 +42,8 @@ class SettingsViewModel @Inject constructor(private val settings: SettingsAdapte
screenOn = settings.keepScreenOn
}
fun updateVictoryPoints(value: Int) {
settings.setVictoryPoints(value)
victoryPoints = settings.victoryPoints
}
}

View File

@@ -12,7 +12,7 @@
<string name="menu_settings">Einstellungen</string>
<string name="on">Ein</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_text">Wirklich den gesamten Verlauf löschen? Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="cancel">Abbrechen</string>
@@ -29,5 +29,11 @@
<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>
</resources>

View File

@@ -15,7 +15,7 @@
<string name="menu_settings">Settings</string>
<string name="on">On</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_text">You really want to delete the history? This action can\'t be undone.</string>
<string name="cancel">Cancel</string>
@@ -28,9 +28,16 @@
<string name="menu_about">About</string>
<string name="contact_us">Contact us</string>
<string name="play_store" translatable="false">Play Store</string>
<string name="continue_play">Continue</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>
</resources>