diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml index bf7d4f2..c390e55 100644 --- a/.idea/androidTestResultsUserPreferences.xml +++ b/.idea/androidTestResultsUserPreferences.xml @@ -3,6 +3,123 @@ diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 1a1d4a0..480dfe4 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -2,10 +2,13 @@ + + + - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 0ad17cb..8978d23 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/schemas/br.com.alexf.boraprofut.data.database.BoraProFutDatabase/1.json b/app/schemas/br.com.alexf.boraprofut.data.database.BoraProFutDatabase/1.json index 4140eec..2ce71d8 100644 --- a/app/schemas/br.com.alexf.boraprofut.data.database.BoraProFutDatabase/1.json +++ b/app/schemas/br.com.alexf.boraprofut.data.database.BoraProFutDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "5a69727f6e08bb3c14f33b339c57fe18", + "identityHash": "7f558be24c60997e8269ee8e2a802965", "entities": [ { "tableName": "PlayerEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `level` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `level` INTEGER NOT NULL, `is_goal_keeper` INTEGER, PRIMARY KEY(`name`))", "fields": [ { "fieldPath": "name", @@ -19,6 +19,12 @@ "columnName": "level", "affinity": "INTEGER", "notNull": true + }, + { + "fieldPath": "isGoalKeeper", + "columnName": "is_goal_keeper", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -34,7 +40,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5a69727f6e08bb3c14f33b339c57fe18')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7f558be24c60997e8269ee8e2a802965')" ] } } \ No newline at end of file diff --git a/app/schemas/br.com.alexf.boraprofut.data.database.BoraProFutDatabase/2.json b/app/schemas/br.com.alexf.boraprofut.data.database.BoraProFutDatabase/2.json new file mode 100644 index 0000000..93edd55 --- /dev/null +++ b/app/schemas/br.com.alexf.boraprofut.data.database.BoraProFutDatabase/2.json @@ -0,0 +1,46 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "be2de52b07ed48f8f8942eed5268e9d4", + "entities": [ + { + "tableName": "PlayerEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `level` INTEGER NOT NULL, `is_goal_keeper` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isGoalKeeper", + "columnName": "is_goal_keeper", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be2de52b07ed48f8f8942eed5268e9d4')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/br/com/alexf/boraprofut/features/PlayersFormScreenTest.kt b/app/src/androidTest/java/br/com/alexf/boraprofut/features/PlayersFormScreenTest.kt deleted file mode 100644 index fd88957..0000000 --- a/app/src/androidTest/java/br/com/alexf/boraprofut/features/PlayersFormScreenTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package br.com.alexf.boraprofut.features - -import androidx.compose.ui.test.assertHasClickAction -import androidx.compose.ui.test.isDisplayed -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import br.com.alexf.boraprofut.features.playersForm.PlayersFormScreen -import br.com.alexf.boraprofut.features.playersForm.PlayersUiState -import org.junit.Rule -import org.junit.Test - -class PlayersFormScreenTest { - - @get:Rule - val composeTestRule = createComposeRule() - - @Test - fun shouldDisplayTitleAndPlayersTextFieldWhenStartsScreen() { - composeTestRule.setContent { - PlayersFormScreen( - uiState = PlayersUiState(), - onClearPlayers = {}, - onSavePlayers = {} - ) - } - composeTestRule.onNodeWithText("Cadastro de jogadores") - .isDisplayed() - composeTestRule.onNodeWithText("Lista de jogadores") - .isDisplayed() - } - - @Test - fun shouldDisplayPlayersCounterWhenInsertAtLeastOnePlayer(){ - composeTestRule.setContent { - PlayersFormScreen( - uiState = PlayersUiState("alex", 1), - onClearPlayers = {}, - onSavePlayers = {} - ) - } - composeTestRule.onNodeWithText("Cadastro de jogadores") - .isDisplayed() - composeTestRule.onNodeWithText("Lista de jogadores") - .isDisplayed() - composeTestRule.onNodeWithText("Jogadores: 1") - .isDisplayed() - } - - @Test - fun shouldDisplayClearButtonWhenInsertSomePlayersInTextField(){ - composeTestRule.setContent { - PlayersFormScreen( - uiState = PlayersUiState( - players = "alex" - ), - onClearPlayers = {}, - onSavePlayers = {} - ) - } - composeTestRule.onNodeWithText("Cadastro de jogadores") - .isDisplayed() - composeTestRule.onNodeWithText("Limpar") - .assertHasClickAction() - .isDisplayed() - } - - @Test - fun shouldDisplaySaveButtonWhenInsertAtLeastFourPlayersInTextField() { - composeTestRule.setContent { - PlayersFormScreen( - uiState = PlayersUiState( - players = "alex\nthaylan\ndaniel\nmaria\n", - amountPlayers = 4, - ), - onClearPlayers = {}, - onSavePlayers = {} - ) - } - composeTestRule.onNodeWithText("Cadastro de jogadores") - .isDisplayed() - composeTestRule.onNodeWithText("Limpar") - .assertHasClickAction() - .isDisplayed() - composeTestRule.onNodeWithText("Jogadores: 4") - .isDisplayed() - composeTestRule.onNodeWithText("Salvar") - .assertHasClickAction() - .isDisplayed() - } - -} \ No newline at end of file diff --git a/app/src/androidTest/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsScreenTest.kt b/app/src/androidTest/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsScreenTest.kt new file mode 100644 index 0000000..9c9befb --- /dev/null +++ b/app/src/androidTest/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsScreenTest.kt @@ -0,0 +1,118 @@ +package br.com.alexf.boraprofut.features.drawTeams + +import androidx.activity.compose.setContent +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import br.com.alexf.boraprofut.MainActivity +import br.com.alexf.boraprofut.R +import br.com.alexf.boraprofut.models.Player +import org.junit.Rule +import org.junit.Test + +private const val PLAYERS_PER_TEAM = 4 + +class DrawTeamsScreenTest { + + @get:Rule + val rule = createAndroidComposeRule() + + private val players = listOf( + Player(name = "goalkeeper", isGoalKeeper = true), + Player(name = "player 1"), + Player(name = "player 2"), + Player(name = "player 3"), + ) + + @Test + fun shouldDisplayAllComponents() { + rule.activity.setContent { + DrawTeamsScreen( + uiState = DrawTeamsUiState( + players = players.toSet(), + playersPerTeam = PLAYERS_PER_TEAM, + isShowPlayers = true + ), + onDrawRandomTeamsClick = {}, + onDrawBalancedTeamsClick = {} + ) {} + } + rule.onNodeWithText(rule.activity.getString(R.string.teams_draw)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.players_amount_per_team)) + .assertIsDisplayed() + rule.onNode( + hasContentDescription(rule.activity.getString(R.string.decreases_players_amount_per_team)) + and + hasClickAction() + ).assertIsDisplayed() + rule.onNodeWithText(PLAYERS_PER_TEAM.toString()) + .assertIsDisplayed() + rule.onNode( + hasContentDescription(rule.activity.getString(R.string.increases_players_amount_per_team)) + and + hasClickAction() + ).assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.random)) + .assertIsDisplayed() + rule.onNodeWithContentDescription(rule.activity.getString(R.string.random)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.balanced)) + .assertIsDisplayed() + rule.onNodeWithContentDescription(rule.activity.getString(R.string.balanced)) + .assertIsDisplayed() + rule.onNodeWithContentDescription( + rule.activity.getString(R.string.icon_of_display_players_button) + ) + .assertIsDisplayed() + .assertHasClickAction() + rule.onNodeWithText("${rule.activity.getString(R.string.hide_players)} (${players.size})") + .assertIsDisplayed() + .assertHasClickAction() + rule.onNodeWithText(rule.activity.getString(R.string.players, players.size)) + .assertIsDisplayed() + rule.onNodeWithTag(EDIT_PLAYERS_BUTTON) + .assertHasClickAction() + .assertIsDisplayed() + rule.onNodeWithTag(PLAYERS_LIST) + .assertIsDisplayed() + rule.onAllNodesWithTag(PLAYERS_LIST_ITEM) + .assertCountEquals(4) + } + + @Test + fun shouldNotDisplayPlayersList_WhenIsShowPlayersIsFalse() { + rule.activity.setContent { + DrawTeamsScreen( + uiState = DrawTeamsUiState( + players = players.toSet(), + playersPerTeam = PLAYERS_PER_TEAM, + isShowPlayers = false + ), + onDrawRandomTeamsClick = {}, + onDrawBalancedTeamsClick = {} + ) {} + } + rule.onNodeWithContentDescription( + rule.activity.getString(R.string.icon_of_hide_players_button) + ).assertIsDisplayed() + rule.onNodeWithText("${rule.activity.getString(R.string.show_players)} (${players.size})") + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.players, players.size)) + .assertIsNotDisplayed() + rule.onNodeWithTag(EDIT_PLAYERS_BUTTON) + .assertIsNotDisplayed() + rule.onNodeWithTag(PLAYERS_LIST) + .assertIsNotDisplayed() + } + + +} \ No newline at end of file diff --git a/app/src/androidTest/java/br/com/alexf/boraprofut/features/drawTeams/PlayersListTest.kt b/app/src/androidTest/java/br/com/alexf/boraprofut/features/drawTeams/PlayersListTest.kt new file mode 100644 index 0000000..e968fae --- /dev/null +++ b/app/src/androidTest/java/br/com/alexf/boraprofut/features/drawTeams/PlayersListTest.kt @@ -0,0 +1,65 @@ +package br.com.alexf.boraprofut.features.drawTeams + +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import br.com.alexf.boraprofut.models.Player +import org.junit.Rule +import org.junit.Test +import kotlin.random.Random + +class PlayersListTest { + + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldDisplayPlayersList() { + val players = listOf( + Player("Goalkeeper", isGoalKeeper = true), + Player("Player", level = Random.nextInt(1, 10)) + ) + rule.setContent { + PlayersList( + uiState = DrawTeamsUiState( + players = players.toSet() + ) + ) + } + rule.onNodeWithText("${players[0].name} (G)").assertIsDisplayed() + rule.onNodeWithText(players[1].name).assertIsDisplayed() + rule.onAllNodesWithTag(PLAYERS_LIST_ITEM).assertCountEquals(2) + } + + @Test + fun shouldDisplayPlayerLevelMenu_WhenPlayerIsNotGoalkeeper() { + rule.setContent { + PlayersList( + uiState = DrawTeamsUiState( + players = setOf( + Player("Player") + ) + ) + ) + } + rule.onNodeWithTag(PLAYER_LEVEL_MENU).assertIsDisplayed() + } + + @Test + fun shouldNotDisplayPlayerLevelMenu_WhenPlayerIsGoalkeeper() { + rule.setContent { + PlayersList( + uiState = DrawTeamsUiState( + players = setOf( + Player("Goalkeeper", isGoalKeeper = true) + ) + ) + ) + } + rule.onNodeWithTag(PLAYER_LEVEL_MENU).assertIsNotDisplayed() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormScreenTest.kt b/app/src/androidTest/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormScreenTest.kt new file mode 100644 index 0000000..c1613f0 --- /dev/null +++ b/app/src/androidTest/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormScreenTest.kt @@ -0,0 +1,154 @@ +package br.com.alexf.boraprofut.features.playersForm + +import androidx.activity.compose.setContent +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.hasClickAction +import androidx.compose.ui.test.hasContentDescription +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import br.com.alexf.boraprofut.MainActivity +import br.com.alexf.boraprofut.R +import kotlinx.coroutines.flow.flowOf +import org.junit.Rule +import org.junit.Test + +class PlayersFormScreenTest { + + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun shouldDisplayTitleAndPlayersTextFieldWhenStartsScreen() { + rule.activity.setContent { + PlayersFormScreen( + uiState = PlayersUiState(), + onClearPlayers = {}, + onSavePlayers = {} + ) + } + rule + .onNodeWithText(rule.activity.getString(R.string.register_of_players)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.players_list)) + .assertIsDisplayed() + } + + @Test + fun shouldDisplayGoalKeeperInfoIcon_WhenStartsScreen() { + rule.activity.setContent { + PlayersFormScreen( + uiState = PlayersUiState(), + onClearPlayers = {}, + onSavePlayers = {} + ) + } + rule.onNode( + hasContentDescription(rule.activity.getString(R.string.show_add_goal_keeper_tip)) + and + hasClickAction() + ).assertIsDisplayed() + } + + @Test + fun shouldDisplayGoalKeeperAlertDialog() { + rule.activity.setContent { + PlayersFormScreen( + uiState = PlayersUiState( + isGoalKeeperToolTipVisible = flowOf(true) + ), + onClearPlayers = {}, + onSavePlayers = {} + ) + } + rule.onNodeWithTag(GOAL_KEEPER_DIALOG) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.add_goal_keeper)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.mark_player_as_goal_keeper_message)) + .assertIsDisplayed() + rule.onNode( + hasText(rule.activity.getString(R.string.got_it)) + and + hasClickAction() + ).assertIsDisplayed() + } + + @Test + fun shouldNotDisplayGoalKeeperAlertDialog() { + rule.activity.setContent { + PlayersFormScreen( + uiState = PlayersUiState( + isGoalKeeperToolTipVisible = flowOf(false) + ), + onClearPlayers = {}, + onSavePlayers = {} + ) + } + rule.onNodeWithTag(GOAL_KEEPER_DIALOG) + .assertIsNotDisplayed() + } + + @Test + fun shouldDisplayPlayersCounterWhenInsertAtLeastOnePlayer() { + rule.activity.setContent { + PlayersFormScreen( + uiState = PlayersUiState(players = "player", 1), + onClearPlayers = {}, + onSavePlayers = {} + ) + } + rule.onNodeWithText(rule.activity.getString(R.string.register_of_players)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.players_list)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.players_registered, 1)) + .assertIsDisplayed() + } + + @Test + fun shouldDisplayClearButtonWhenInsertSomePlayersInTextField() { + rule.activity.setContent { + PlayersFormScreen( + uiState = PlayersUiState( + players = "player" + ), + onClearPlayers = {}, + onSavePlayers = {} + ) + } + rule.onNodeWithText(rule.activity.getString(R.string.register_of_players)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.clear)) + .assertHasClickAction() + .assertIsDisplayed() + } + + @Test + fun shouldDisplaySaveButtonWhenInsertAtLeastFourPlayersInTextField() { + rule.activity.setContent { + PlayersFormScreen( + uiState = PlayersUiState( + players = "player 1\nplayer 2\nplayer 3\nplayer 4\n", + amountPlayers = 4, + ), + onClearPlayers = {}, + onSavePlayers = {} + ) + } + rule.onNodeWithText(rule.activity.getString(R.string.register_of_players)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.clear)) + .assertHasClickAction() + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.players_registered, 4)) + .assertIsDisplayed() + rule.onNodeWithText(rule.activity.getString(R.string.save)) + .assertHasClickAction() + .assertIsDisplayed() + } + +} \ No newline at end of file diff --git a/app/src/main/java/br/com/alexf/boraprofut/data/database/BoraProFutDatabase.kt b/app/src/main/java/br/com/alexf/boraprofut/data/database/BoraProFutDatabase.kt index 9e3a9c6..49b487a 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/data/database/BoraProFutDatabase.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/data/database/BoraProFutDatabase.kt @@ -1,14 +1,16 @@ package br.com.alexf.boraprofut.data.database +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase import br.com.alexf.boraprofut.data.database.dao.PlayersDao import br.com.alexf.boraprofut.data.database.entities.PlayerEntity @Database( - version = 1, + version = 2, exportSchema = true, - entities = [PlayerEntity::class] + entities = [PlayerEntity::class], + autoMigrations = [AutoMigration(from = 1, to = 2)] ) abstract class BoraProFutDatabase : RoomDatabase(){ diff --git a/app/src/main/java/br/com/alexf/boraprofut/data/database/dao/PlayersDao.kt b/app/src/main/java/br/com/alexf/boraprofut/data/database/dao/PlayersDao.kt index 8563669..bb893c2 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/data/database/dao/PlayersDao.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/data/database/dao/PlayersDao.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow @Dao interface PlayersDao { - @Query("SELECT * FROM PlayerEntity ORDER by name") + @Query("SELECT * FROM PlayerEntity ORDER BY is_goal_keeper DESC, name") fun findAll(): Flow> @Insert(onConflict = OnConflictStrategy.REPLACE) diff --git a/app/src/main/java/br/com/alexf/boraprofut/data/database/entities/PlayerEntity.kt b/app/src/main/java/br/com/alexf/boraprofut/data/database/entities/PlayerEntity.kt index 6c0df68..3e78a37 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/data/database/entities/PlayerEntity.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/data/database/entities/PlayerEntity.kt @@ -1,5 +1,6 @@ package br.com.alexf.boraprofut.data.database.entities +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @@ -8,4 +9,6 @@ class PlayerEntity( @PrimaryKey val name: String, val level: Int, + @ColumnInfo("is_goal_keeper") + val isGoalKeeper: Boolean = false ) \ No newline at end of file diff --git a/app/src/main/java/br/com/alexf/boraprofut/data/repositories/PlayersFormPreferencesRepository.kt b/app/src/main/java/br/com/alexf/boraprofut/data/repositories/PlayersFormPreferencesRepository.kt new file mode 100644 index 0000000..2187d99 --- /dev/null +++ b/app/src/main/java/br/com/alexf/boraprofut/data/repositories/PlayersFormPreferencesRepository.kt @@ -0,0 +1,34 @@ +package br.com.alexf.boraprofut.data.repositories + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class PlayersFormPreferencesRepository(private val dataStore: DataStore) { + + suspend fun showGoalKeeperToolTip() { + dataStore.edit { preferences -> + preferences[IS_GOAL_KEEPER_TOOL_TIP_VISIBLE] = true + } + } + + suspend fun hideGoalKeeperToolTip() { + dataStore.edit { preferences -> + preferences[IS_GOAL_KEEPER_TOOL_TIP_VISIBLE] = false + } + } + + fun isGoalKeeperToolTipVisible(): Flow { + return dataStore.data.map { preferences -> + preferences[IS_GOAL_KEEPER_TOOL_TIP_VISIBLE] ?: true + } + } + + companion object { + private val IS_GOAL_KEEPER_TOOL_TIP_VISIBLE = + booleanPreferencesKey("isGoalkeeperToolTipVisible") + } +} \ No newline at end of file diff --git a/app/src/main/java/br/com/alexf/boraprofut/data/repositories/PlayersRepository.kt b/app/src/main/java/br/com/alexf/boraprofut/data/repositories/PlayersRepository.kt index aa92a8a..426bc5a 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/data/repositories/PlayersRepository.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/data/repositories/PlayersRepository.kt @@ -13,11 +13,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext private val playersPerTeamPreference = intPreferencesKey("playersPerTeam") -private const val defaultPlayersPerTeam = 5 +private const val DEFAULT_PLAYERS_PER_TEAM = 5 class PlayersRepository( private val dao: PlayersDao, @@ -28,7 +27,7 @@ class PlayersRepository( val games = _game.asStateFlow() val playersPerTeam get() = dataStore.data.map { - it[playersPerTeamPreference] ?: defaultPlayersPerTeam + it[playersPerTeamPreference] ?: DEFAULT_PLAYERS_PER_TEAM } suspend fun save(players: Set) { @@ -75,15 +74,6 @@ class PlayersRepository( } } - fun saveGame(players: Set) { - _game.update { - players - .filter { - it.name.trim().isNotBlank() - }.toSet() - } - } - suspend fun deleteAllPlayers() { dao.deleteAllPlayers() } @@ -95,8 +85,11 @@ class PlayersRepository( } private fun Player.toPlayerEntity(): PlayerEntity { + val name = this.name.replace(Regex("\\([Gg]\\)"), "") + val isGoalKeeper = this.name.contains(Regex("\\([Gg]\\)")) || this.isGoalKeeper return PlayerEntity( - name = this.name, - level = this.level + name = name, + level = this.level, + isGoalKeeper = isGoalKeeper ) } diff --git a/app/src/main/java/br/com/alexf/boraprofut/di/AppModules.kt b/app/src/main/java/br/com/alexf/boraprofut/di/AppModules.kt index 529bb1b..f56acca 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/di/AppModules.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/di/AppModules.kt @@ -5,12 +5,13 @@ import androidx.datastore.preferences.preferencesDataStoreFile import androidx.room.Room import br.com.alexf.boraprofut.data.database.BoraProFutDatabase import br.com.alexf.boraprofut.data.repositories.PlayersRepository +import br.com.alexf.boraprofut.data.repositories.PlayersFormPreferencesRepository import br.com.alexf.boraprofut.features.balancedTeams.BalancedTeamViewModel import br.com.alexf.boraprofut.features.drawTeams.DrawTeamsViewModel import br.com.alexf.boraprofut.features.drawTeams.useCases.TeamDrawerUseCase +import br.com.alexf.boraprofut.features.game.GameViewModel import br.com.alexf.boraprofut.features.game.usecase.GameUseCase import br.com.alexf.boraprofut.features.playersForm.PlayersFormViewModel -import br.com.alexf.boraprofut.features.game.GameViewModel import br.com.alexf.boraprofut.features.randomteams.RandomTeamsViewModel import br.com.alexf.boraprofut.features.timer.TimerCountDown import br.com.alexf.boraprofut.features.timer.TimerViewModel @@ -33,6 +34,7 @@ val appModule = module { val dataModule = module { singleOf(::PlayersRepository) + singleOf(::PlayersFormPreferencesRepository) single { Room.databaseBuilder( androidContext(), diff --git a/app/src/main/java/br/com/alexf/boraprofut/features/balancedTeams/BalancedTeamsScreen.kt b/app/src/main/java/br/com/alexf/boraprofut/features/balancedTeams/BalancedTeamsScreen.kt index a22abd8..bfb9604 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/features/balancedTeams/BalancedTeamsScreen.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/features/balancedTeams/BalancedTeamsScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import br.com.alexf.boraprofut.R @@ -98,27 +97,29 @@ fun BalancedTeamsScreen( ) } Column { - team.players.forEach { p -> + team.players.forEach { player -> Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = p.name, + text = "${player.name} ${if (player.isGoalKeeper) "(G)" else ""}", Modifier.padding(horizontal = 16.dp, vertical = 8.dp), style = LocalTextStyle.current.copy( fontSize = 20.sp, fontWeight = FontWeight.Bold ) ) - Text( - text = "${p.level}", - Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - style = LocalTextStyle.current.copy( - fontSize = 20.sp, - fontWeight = FontWeight.Bold + if (!player.isGoalKeeper) { + Text( + text = "${player.level}", + Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = LocalTextStyle.current.copy( + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) ) - ) + } } } } diff --git a/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsScreen.kt b/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsScreen.kt index 7acbd47..3adf6e4 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsScreen.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -28,7 +29,10 @@ import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Remove +import androidx.compose.material.icons.outlined.SportsHandball +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -42,11 +46,11 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import br.com.alexf.boraprofut.R @@ -66,6 +70,11 @@ import br.com.alexf.boraprofut.ui.theme.PlayersContainerPrimaryColor import br.com.alexf.boraprofut.ui.theme.PlayersContainerSecondaryColor import kotlin.random.Random +const val PLAYERS_LIST = "PlayersList" +const val PLAYERS_LIST_ITEM = "PlayersListItem" +const val PLAYER_LEVEL_MENU = "PlayerLevelMenu" +const val EDIT_PLAYERS_BUTTON = "EditPlayersButton" + private class DrawOption( val title: String, val icon: ImageVector, @@ -73,7 +82,6 @@ private class DrawOption( val action: () -> Unit ) -@OptIn(ExperimentalFoundationApi::class) @Composable fun DrawTeamsScreen( uiState: DrawTeamsUiState, @@ -84,6 +92,7 @@ fun DrawTeamsScreen( ) { val context = LocalContext.current val totalPlayers = uiState.players.size + Column( modifier .fillMaxSize() @@ -104,8 +113,6 @@ fun DrawTeamsScreen( .align(Alignment.CenterHorizontally) ) val options = remember { - - listOf( DrawOption( title = context.getString(R.string.random), @@ -161,7 +168,7 @@ fun DrawTeamsScreen( ) Spacer(modifier = Modifier.size(16.dp)) Icon( - option.icon, contentDescription = null, + option.icon, contentDescription = option.title, Modifier.size(64.dp), tint = Color.White ) @@ -207,154 +214,202 @@ fun DrawTeamsScreen( ) } if (uiState.isShowPlayers) { - Column { - Row( - Modifier - .fillMaxWidth() - .background( - Brush.linearGradient( - listOf( - PlayersContainerPrimaryColor, - PlayersContainerSecondaryColor, - MaterialTheme.colorScheme.background - ), - ) - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = stringResource(R.string.players), - Modifier.padding(16.dp), - style = MaterialTheme.typography.titleLarge.copy(Color.White) - ) - + if (uiState.players.isNotEmpty()) { + Column { Row( Modifier - .padding(16.dp) - .clickable { - onEditPlayersClick() - } - .clip(RoundedCornerShape(15)) + .fillMaxWidth() .background( Brush.linearGradient( listOf( - EditPlayersButtonContainerPrimaryColor, - EditPlayersButtonContainerSecondaryColor, - ) + PlayersContainerPrimaryColor, + PlayersContainerSecondaryColor, + MaterialTheme.colorScheme.background + ), ) - ) - .padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - Icon( - imageVector = Icons.Filled.Edit, - contentDescription = stringResource(R.string.players_edit_icon), - tint = Color.White - ) Text( - text = stringResource(R.string.edit), - style = LocalTextStyle.current.copy(color = Color.White) + text = stringResource(R.string.players, uiState.players.size), + Modifier.padding(16.dp), + style = MaterialTheme.typography.titleLarge.copy(Color.White) ) - } - } - Column( - Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - uiState.players.forEach { player -> + Row( Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = player.name, Modifier.weight(1f), - style = LocalTextStyle.current.copy( - fontSize = 20.sp, - fontWeight = FontWeight.Bold - ) - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - - Box( - modifier = Modifier - .clip(CircleShape) - .combinedClickable( - onClick = { - uiState.onDecreasePlayerLevel(player) - }, - onLongClick = { - uiState.onDecreasePlayerLevel(player) - } - ) - .background( - DecreasePlayerLevelContainerColor - .copy(alpha = 0.8f) - ) - .padding(4.dp) - ) { - Icon( - Icons.Outlined.Remove, - contentDescription = null, - tint = Color.White - ) + .padding(16.dp) + .clickable { + onEditPlayersClick() } - Text( - text = "${player.level}", - Modifier.width(30.dp), - style = LocalTextStyle.current.copy( - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ) - ) - - Box( - modifier = Modifier - .clip(CircleShape) - .combinedClickable( - onClick = { - uiState.onIncreasePlayerLevel(player) - }, - onLongClick = { - - uiState.onIncreasePlayerLevel(player) - } - ) - .background( - IncreasePlayerLevelContainerColor - .copy(alpha = 0.8f) + .clip(RoundedCornerShape(15)) + .background( + Brush.linearGradient( + listOf( + EditPlayersButtonContainerPrimaryColor, + EditPlayersButtonContainerSecondaryColor, ) - .padding(4.dp) - ) { - Icon( - Icons.Outlined.Add, - contentDescription = null, - tint = Color.White ) - } - } + ) + .padding(8.dp) + .testTag(EDIT_PLAYERS_BUTTON), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Filled.Edit, + contentDescription = stringResource(R.string.players_edit_icon), + tint = Color.White + ) + Text( + text = stringResource(R.string.edit), + style = LocalTextStyle.current.copy(color = Color.White) + ) } } - + PlayersList(uiState = uiState) } } } } } +@Composable +fun PlayersList(uiState: DrawTeamsUiState) { + Column(Modifier.testTag(PLAYERS_LIST)) { + uiState.players.forEachIndexed { index, player -> + PlayersListItem(uiState = uiState, player = player) + if (index < uiState.players.size - 1) { + HorizontalDivider() + } + } + } +} + +@Composable +fun PlayersListItem(uiState: DrawTeamsUiState, player: Player) { + Row( + Modifier + .fillMaxWidth() + .testTag(PLAYERS_LIST_ITEM), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = { + uiState.onGoalKeeperChange(player) + }, modifier = Modifier.padding(vertical = 16.dp)) { + if (player.isGoalKeeper == true) { + Icon( + imageVector = Icons.Outlined.SportsHandball, + contentDescription = stringResource(R.string.goal_keeper_icon) + ) + } else { + Icon( + imageVector = Icons.Outlined.SportsHandball, + contentDescription = stringResource(R.string.not_goal_keeper_icon), + tint = if (isSystemInDarkTheme()) { + Color.White.copy(alpha = 0.2f) + } else { + Color.Black.copy(alpha = 0.2f) + }, + ) + } + + } + Text( + text = if (player.isGoalKeeper == true) { + "${player.name.trim()} (G)" + } else { + player.name + }, + Modifier.weight(1f), + style = LocalTextStyle.current.copy( + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + ) + if (player.isGoalKeeper == false) { + PlayerLevelMenu(uiState = uiState, player = player) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun PlayerLevelMenu(uiState: DrawTeamsUiState, player: Player) { + Row( + Modifier + .padding(end = 16.dp) + .testTag(PLAYER_LEVEL_MENU), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Box( + modifier = Modifier + .clip(CircleShape) + .combinedClickable( + onClick = { + uiState.onDecreasePlayerLevel(player) + }, + onLongClick = { + uiState.onDecreasePlayerLevel(player) + } + ) + .background( + DecreasePlayerLevelContainerColor + .copy(alpha = 0.8f) + ) + .padding(4.dp) + ) { + Icon( + Icons.Outlined.Remove, + contentDescription = stringResource(id = R.string.decrease_level), + tint = Color.White + ) + } + Text( + text = "${player.level}", + Modifier.width(28.dp), + style = LocalTextStyle.current.copy( + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ) + ) + + Box( + modifier = Modifier + .clip(CircleShape) + .combinedClickable( + onClick = { + uiState.onIncreasePlayerLevel(player) + }, + onLongClick = { + + uiState.onIncreasePlayerLevel(player) + } + ) + .background( + IncreasePlayerLevelContainerColor + .copy(alpha = 0.8f) + ) + .padding(4.dp) + ) { + Icon( + Icons.Outlined.Add, + contentDescription = stringResource(id = R.string.increase_level), + tint = Color.White + ) + } + } +} @UiModePreviews @Composable fun DrawTeamsScreenPreview() { BoraProFutTheme { DrawTeamsScreen( - uiState = DrawTeamsUiState(), + uiState = DrawTeamsUiState(isShowPlayers = false), onDrawRandomTeamsClick = {}, onDrawBalancedTeamsClick = {}, onEditPlayersClick = {} @@ -362,6 +417,29 @@ fun DrawTeamsScreenPreview() { } } +@UiModePreviews +@Composable +fun PlayersListPreview() { + + val players = mutableSetOf( + Player(name = "Alex", level = Random.nextInt(1, 10), isGoalKeeper = true), + Player(name = "Thailan", level = Random.nextInt(1, 10)), + Player(name = "Daniel", level = Random.nextInt(1, 10)), + Player(name = "Joao", level = 10), + Player(name = "Janssen", level = Random.nextInt(1, 10)) + ) + + BoraProFutTheme { + Surface { + PlayersList( + uiState = DrawTeamsUiState( + players = players + ) + ) + } + } +} + @UiModePreviews @Composable fun DrawTeamsScreenDisplayingPlayersPreview() { @@ -370,7 +448,11 @@ fun DrawTeamsScreenDisplayingPlayersPreview() { DrawTeamsScreen( uiState = DrawTeamsUiState( players = setOf( - Player(name = "Alex", level = Random.nextInt(1, 10)), + Player( + name = "Alex", + level = Random.nextInt(1, 10), + isGoalKeeper = true + ), Player(name = "Thailan", level = Random.nextInt(1, 10)), Player(name = "Daniel", level = Random.nextInt(1, 10)), Player(name = "Joao", level = 10), diff --git a/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsViewModel.kt b/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsViewModel.kt index 29f492c..5c2fa3b 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsViewModel.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/DrawTeamsViewModel.kt @@ -24,6 +24,7 @@ data class DrawTeamsUiState( val onIncreasePlayersPerTeam: () -> Unit = {}, val onIncreasePlayerLevel: (Player) -> Unit = {}, val onDecreasePlayerLevel: (Player) -> Unit = {}, + val onGoalKeeperChange: (Player) -> Unit = {}, val initState: InitState = InitState.LOADING ) @@ -70,6 +71,12 @@ class DrawTeamsViewModel( viewModelScope.launch { repository.increasePlayerLevel(it) } + }, + onGoalKeeperChange = { + val player = it.copy(isGoalKeeper = !it.isGoalKeeper) + viewModelScope.launch { + repository.save(setOf(player)) + } } ) } diff --git a/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/useCases/TeamDrawerUseCase.kt b/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/useCases/TeamDrawerUseCase.kt index 7821557..90ce754 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/useCases/TeamDrawerUseCase.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/features/drawTeams/useCases/TeamDrawerUseCase.kt @@ -1,7 +1,6 @@ package br.com.alexf.boraprofut.features.drawTeams.useCases import br.com.alexf.boraprofut.models.Player -import kotlin.math.ceil class TeamDrawerUseCase { @@ -9,40 +8,72 @@ class TeamDrawerUseCase { players: Set, playersPerTeam: Int ): List> { - return players - .shuffled() - .chunked(playersPerTeam) { - it.toSet() - } + return getTeams(players = players, playersPerTeam = playersPerTeam) } fun drawBalancedTeams( players: Set, playersPerTeam: Int ): List> { - val amountTeams = ceil(players.size / playersPerTeam.toFloat()).toInt() - val sortedPlayers = players.shuffled() - .sortedBy { - it.level - }.toMutableList() - - val teamsWithBestPlayersIncluded = MutableList(amountTeams) { - val bestPlayer = sortedPlayers.removeLast() - mutableListOf(bestPlayer) + return getTeams(isBalancedTeam = true, players = players, playersPerTeam = playersPerTeam) + } + + private fun getTeams( + isBalancedTeam: Boolean = false, + players: Set, + playersPerTeam: Int + ): List> { + val onlyGoalKeepers = players + .filter { it.isGoalKeeper } + .shuffled() + .toMutableList() + val onlyPlayers = if (isBalancedTeam) { + players.filter { !it.isGoalKeeper }.sortedByDescending { it.level }.toMutableList() + } else { + players.filter { !it.isGoalKeeper }.shuffled().toMutableList() } + val totalOfGoalKeepers = onlyGoalKeepers.size + val totalOfTeams: Int = players.size / playersPerTeam + val teams: MutableList> = + MutableList(totalOfTeams) { mutableListOf() } + var firstTeamWithoutGoalKeeper = 0 - while (sortedPlayers.isNotEmpty()) { - teamsWithBestPlayersIncluded.forEach { - if (sortedPlayers.isEmpty()) { - return@forEach + // add players to the teams + if (teams.isNotEmpty()) { + for (current in 0 until playersPerTeam - 1) { + teams.forEach { team -> + if (onlyPlayers.isNotEmpty() && team.size < playersPerTeam) { + team.add(onlyPlayers.removeFirst()) + } } - it.add(sortedPlayers.removeFirst()) } } - return teamsWithBestPlayersIncluded.map { - it.toSet() + // add goalkeepers to the teams + teams.forEachIndexed { index, team -> + if (onlyGoalKeepers.isNotEmpty()) { + team.add(0, onlyGoalKeepers.removeFirst()) + firstTeamWithoutGoalKeeper = index + 1 + } + } + + // when the player list has only one goalkeeper, add the remaining players to the teams without goalkeepers + if (totalOfGoalKeepers <= 1) { + teams.subList(firstTeamWithoutGoalKeeper, teams.size).forEach { + if (onlyPlayers.isNotEmpty()) + it.add(onlyPlayers.removeFirst()) + } } + + // when players remain, add to a new team + if (onlyPlayers.isNotEmpty()) { + teams.add(onlyPlayers) + if (onlyGoalKeepers.isNotEmpty()) { + teams.last().add(0, onlyGoalKeepers.removeFirst()) + } + } + + return teams.map { it.toSet() } } } diff --git a/app/src/main/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormScreen.kt b/app/src/main/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormScreen.kt index 742c5a7..38a3e47 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormScreen.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormScreen.kt @@ -18,17 +18,26 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.outlined.Clear import androidx.compose.material.icons.outlined.Done +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -41,6 +50,9 @@ import br.com.alexf.boraprofut.ui.theme.ClearPlayersTextFieldContainerColor import br.com.alexf.boraprofut.ui.theme.DuplicatesNamesContainerColor import br.com.alexf.boraprofut.ui.theme.SavePlayersButtonContainerColor +const val GOAL_KEEPER_DIALOG = "GoalKeeperDialog" + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PlayersFormScreen( uiState: PlayersUiState, @@ -48,145 +60,200 @@ fun PlayersFormScreen( onClearPlayers: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .background(MaterialTheme.colorScheme.background) - ) { + val isGoalKeeperToolTipVisible = uiState.isGoalKeeperToolTipVisible.collectAsState(initial = true) + Scaffold( + topBar = { + TopAppBar(title = { + Text(text = stringResource(id = R.string.register_of_players)) + }, + actions = { + IconButton(onClick = uiState.onShowGoalKeeperToolTip) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = stringResource(R.string.show_add_goal_keeper_tip), + ) + } + }) + } + ) { paddingValues -> Column( - Modifier - .weight(1f) + modifier + .padding(paddingValues) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .background(MaterialTheme.colorScheme.background) ) { - Text( - text = stringResource(id = R.string.register_of_players), - Modifier.padding(16.dp), - style = MaterialTheme.typography.titleLarge - ) - AnimatedVisibility( - visible = uiState.players.isNotBlank(), - enter = fadeIn(initialAlpha = 0.0f) + Column( + Modifier + .weight(1f) ) { - Row( - modifier.padding( - top = 10.dp, start = 16.dp, end = 16.dp - ) + AnimatedVisibility( + visible = uiState.players.isNotBlank(), + enter = fadeIn(initialAlpha = 0.0f) ) { - Text( - text = stringResource(R.string.players_registered), - fontWeight = FontWeight(700) - ) - Text( - text = "${uiState.amountPlayers}", - Modifier.padding(start = 8.dp), - ) + Row( + modifier.padding( + top = 10.dp, start = 16.dp, end = 16.dp + ) + ) { + Text( + text = stringResource(R.string.players_registered), + fontWeight = FontWeight(700) + ) + Text( + text = "${uiState.amountPlayers}", + Modifier.padding(start = 8.dp), + ) + } } - } - AnimatedVisibility( - visible = uiState.duplicateNames.isNotEmpty(), - enter = fadeIn(initialAlpha = 0.0f) - ) { - Column(Modifier.padding(horizontal = 16.dp)) { - Text( - text = stringResource(id = R.string.names_duplicated), - Modifier.padding(top = 10.dp), - fontWeight = FontWeight(700) - ) + AnimatedVisibility( + visible = uiState.duplicateNames.isNotEmpty(), + enter = fadeIn(initialAlpha = 0.0f) + ) { + Column(Modifier.padding(horizontal = 16.dp)) { + Text( + text = stringResource(id = R.string.names_duplicated), + Modifier.padding(top = 10.dp), + fontWeight = FontWeight(700) + ) - Text( - text = uiState.duplicateNames, Modifier - .clip(RoundedCornerShape(8.dp)) - .background(DuplicatesNamesContainerColor) - .padding( - horizontal = 12.dp, - vertical = 8.dp - ), color = Color.White, fontSize = 12.sp - ) + Text( + text = uiState.duplicateNames, Modifier + .clip(RoundedCornerShape(8.dp)) + .background(DuplicatesNamesContainerColor) + .padding( + horizontal = 12.dp, + vertical = 8.dp + ), color = Color.White, fontSize = 12.sp + ) + } } - } - Column( - Modifier.padding(top = 16.dp) - ) { - Row( - Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp) + Column( + Modifier.padding(top = 16.dp) ) { - - AnimatedVisibility( - uiState.players.isNotBlank(), - modifier, - fadeIn(initialAlpha = 0.0f) + Row( + Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - Row( - Modifier - .clip(CircleShape) - .background(ClearPlayersTextFieldContainerColor) - .padding(horizontal = 8.dp, vertical = 8.dp) - .clickable { onClearPlayers() }, - horizontalArrangement = Arrangement.Absolute.Center, - verticalAlignment = Alignment.CenterVertically + AnimatedVisibility( + uiState.players.isNotBlank(), + modifier, + fadeIn(initialAlpha = 0.0f) ) { - Text( - stringResource(id = R.string.clear), - color = Color.White, - fontWeight = FontWeight(700) - ) - Spacer(Modifier.size(4.dp)) - Icon( - imageVector = Icons.Outlined.Clear, - contentDescription = null, - Modifier.clip(CircleShape), - tint = Color.White, - ) - } - } - AnimatedVisibility( - uiState.isShowSaveButton(), - modifier.weight(1f), - fadeIn(initialAlpha = 0.0f) - ) { + Row( + Modifier + .clip(CircleShape) + .background(ClearPlayersTextFieldContainerColor) + .padding(horizontal = 8.dp, vertical = 8.dp) + .clickable { onClearPlayers() }, + horizontalArrangement = Arrangement.Absolute.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + stringResource(id = R.string.clear), + color = Color.White, + fontWeight = FontWeight(700) + ) + Spacer(Modifier.size(4.dp)) + Icon( + imageVector = Icons.Outlined.Clear, + contentDescription = null, + Modifier.clip(CircleShape), + tint = Color.White, + ) + } + } - Row( - Modifier - .clip(CircleShape) - .background(SavePlayersButtonContainerColor) - .padding(horizontal = 8.dp, vertical = 8.dp) - .clickable { onSavePlayers() }, - horizontalArrangement = Arrangement.Absolute.Center, - verticalAlignment = Alignment.CenterVertically + AnimatedVisibility( + uiState.isShowSaveButton(), + modifier.weight(1f), + fadeIn(initialAlpha = 0.0f) ) { - Text( - stringResource(id = R.string.save), - color = Color.White, - fontWeight = FontWeight(700) - ) - Spacer(Modifier.size(4.dp)) - Icon( - imageVector = Icons.Outlined.Done, - contentDescription = null, - Modifier.clip(CircleShape), - tint = Color.White, - ) + + Row( + Modifier + .clip(CircleShape) + .background(SavePlayersButtonContainerColor) + .padding(horizontal = 8.dp, vertical = 8.dp) + .clickable { onSavePlayers() }, + horizontalArrangement = Arrangement.Absolute.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + stringResource(id = R.string.save), + color = Color.White, + fontWeight = FontWeight(700) + ) + Spacer(Modifier.size(4.dp)) + Icon( + imageVector = Icons.Outlined.Done, + contentDescription = null, + Modifier.clip(CircleShape), + tint = Color.White, + ) + } } } } + OutlinedTextField( + value = uiState.players, + onValueChange = uiState.onPlayersChange, + Modifier + .heightIn(200.dp) + .fillMaxWidth() + .padding(16.dp), + label = { Text(text = stringResource(R.string.players_list)) }, + shape = RoundedCornerShape(4) + ) + if (isGoalKeeperToolTipVisible.value) { + GoalKeeperToolTipAlert( + onDismissRequest = uiState.onHideGoalKeeperToolTip, + onConfirmation = uiState.onHideGoalKeeperToolTip, + dialogTitle = stringResource(R.string.add_goal_keeper), + dialogText = stringResource(id = R.string.mark_player_as_goal_keeper_message), + dialogButtonText = stringResource(R.string.got_it) + ) + } } - OutlinedTextField( - value = uiState.players, - onValueChange = uiState.onPlayersChange, - Modifier - .heightIn(200.dp) - .fillMaxWidth() - .padding(16.dp), - label = { Text(text = stringResource(R.string.players_list)) }, - shape = RoundedCornerShape(4) - ) } } + +} + +@Composable +fun GoalKeeperToolTipAlert( + onDismissRequest: () -> Unit, + onConfirmation: () -> Unit, + dialogTitle: String, + dialogText: String, + dialogButtonText: String +) { + AlertDialog( + title = { Text(text = dialogTitle) }, + text = { Text(text = dialogText) }, + modifier = Modifier.testTag(GOAL_KEEPER_DIALOG), + onDismissRequest = { onDismissRequest() }, + confirmButton = { + TextButton(onClick = { onConfirmation() }) { + Text(text = dialogButtonText) + } + }) +} + +@Preview +@Composable +fun GoalKeeperToolTipAlertPreview() { + GoalKeeperToolTipAlert( + onDismissRequest = {}, + onConfirmation = {}, + dialogTitle = "Adicionar Goleiro", + dialogText = "Para adicionar um goleiro...", + dialogButtonText = "Entendi" + ) } @UiModePreviews diff --git a/app/src/main/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormViewModel.kt b/app/src/main/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormViewModel.kt index b34d12c..adc49b9 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormViewModel.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/features/playersForm/PlayersFormViewModel.kt @@ -3,12 +3,15 @@ package br.com.alexf.boraprofut.features.playersForm import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import br.com.alexf.boraprofut.data.repositories.PlayersRepository +import br.com.alexf.boraprofut.data.repositories.PlayersFormPreferencesRepository import br.com.alexf.boraprofut.models.Player +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -20,6 +23,9 @@ data class PlayersUiState( val duplicateNames: String = "", val onPlayersChange: (String) -> Unit = {}, val isSaving: Boolean = false, + val isGoalKeeperToolTipVisible: Flow = flowOf(false), + val onShowGoalKeeperToolTip: () -> Unit = {}, + val onHideGoalKeeperToolTip: () -> Unit = {} ) { fun isShowSaveButton() = players.isNotBlank() && amountPlayers != null && amountPlayers > 0 @@ -27,7 +33,8 @@ data class PlayersUiState( } class PlayersFormViewModel( - private val repository: PlayersRepository + private val repository: PlayersRepository, + private val playersFormPreferencesRepository: PlayersFormPreferencesRepository ) : ViewModel() { private val _uiState = MutableStateFlow(PlayersUiState()) @@ -38,24 +45,44 @@ class PlayersFormViewModel( init { _uiState.update { currentState -> currentState.copy( + isGoalKeeperToolTipVisible = playersFormPreferencesRepository.isGoalKeeperToolTipVisible(), onPlayersChange = { players -> _uiState.update { it.copy( players = players, amountPlayers = isTherePlayer(players), duplicateNames = players.duplicateNames() - .joinToString() + .joinToString(), ) } }, + onShowGoalKeeperToolTip = { + viewModelScope.launch { + playersFormPreferencesRepository.showGoalKeeperToolTip() + } + }, + onHideGoalKeeperToolTip = { + viewModelScope.launch { + playersFormPreferencesRepository.hideGoalKeeperToolTip() + } + } ) } + + viewModelScope.launch { repository.players.collectLatest { players -> _uiState.update { currentState -> currentState.copy( - players = players.joinToString("") { "${it.name}\n" }, + players = players + .joinToString("") { + if (it.isGoalKeeper) { + "${it.name.trim()} (G)\n" + } else { + "${it.name}\n" + } + }, amountPlayers = players.size ) } diff --git a/app/src/main/java/br/com/alexf/boraprofut/features/randomteams/RandomTeamsScreen.kt b/app/src/main/java/br/com/alexf/boraprofut/features/randomteams/RandomTeamsScreen.kt index a4c321a..b1f8638 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/features/randomteams/RandomTeamsScreen.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/features/randomteams/RandomTeamsScreen.kt @@ -53,7 +53,7 @@ fun RandomTeamsScreen( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Times sorteados", + text = stringResource(R.string.teams_drawn), Modifier .weight(1f), style = MaterialTheme.typography.titleLarge @@ -85,40 +85,42 @@ fun RandomTeamsScreen( horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "Time ${index + 1}", + text = stringResource(id = R.string.team, index + 1), Modifier .padding(16.dp), style = MaterialTheme.typography.titleMedium ) Text( - text = "Nível ${team.level}", + text = stringResource(id = R.string.level, team.level), Modifier .padding(16.dp), style = MaterialTheme.typography.titleMedium ) } Column { - team.players.forEach { p -> + team.players.forEach { player -> Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = p.name, + text = "${player.name} ${if (player.isGoalKeeper) "(G)" else ""}", Modifier.padding(horizontal = 16.dp, vertical = 8.dp), style = LocalTextStyle.current.copy( fontSize = 20.sp, fontWeight = FontWeight.Bold ) ) - Text( - text = "${p.level}", - Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - style = LocalTextStyle.current.copy( - fontSize = 20.sp, - fontWeight = FontWeight.Bold + if (!player.isGoalKeeper) { + Text( + text = "${player.level}", + Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + style = LocalTextStyle.current.copy( + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) ) - ) + } } } } diff --git a/app/src/main/java/br/com/alexf/boraprofut/models/Player.kt b/app/src/main/java/br/com/alexf/boraprofut/models/Player.kt index 6061e59..17c5055 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/models/Player.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/models/Player.kt @@ -1,6 +1,10 @@ package br.com.alexf.boraprofut.models +import androidx.room.ColumnInfo + data class Player( val name: String, - val level: Int = 0 + val level: Int = 0, + @ColumnInfo("is_goal_keeper") + val isGoalKeeper: Boolean = false ) \ No newline at end of file diff --git a/app/src/main/java/br/com/alexf/boraprofut/ui/theme/Color.kt b/app/src/main/java/br/com/alexf/boraprofut/ui/theme/Color.kt index 9105e76..2c69a24 100644 --- a/app/src/main/java/br/com/alexf/boraprofut/ui/theme/Color.kt +++ b/app/src/main/java/br/com/alexf/boraprofut/ui/theme/Color.kt @@ -10,10 +10,8 @@ val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) - val SelectPlayerContainerPrimaryColor = Color(0xFF673AB7) val SelectPlayerContainerSecondaryColor = Color(0xFFF44336) - val DrawRandomTeamsContainerPrimaryColor = Color(0xFF673AB7) val DrawRandomTeamsContainerSecondaryColor = Color(0xFF223311) val DrawBalancedTeamsContainerPrimaryColor = Color(0xFF2196F3) @@ -24,12 +22,10 @@ val EditPlayersButtonContainerPrimaryColor = Color(0xFFD500F9) val EditPlayersButtonContainerSecondaryColor = Color(0xFF651FFF) val DecreasePlayerLevelContainerColor = Color(0xFFFF1744) val IncreasePlayerLevelContainerColor = Color(0xFF00E676) - -val DuplicatesNamesContainerColor = Color(0xFF8B0000) -val ClearPlayersTextFieldContainerColor = Color(0xFF8B0000) -val SavePlayersButtonContainerColor = Color(0xFF006400) - val PauseButtonColor = Color(0xFFE53935) val ContinueButtonColor = Color(0xFF008866) val RestartButtonColor = Color(0xFFFDC417) -val RestartButtonContentColor = Color(0xFF424242) \ No newline at end of file +val RestartButtonContentColor = Color(0xFF424242) +val DuplicatesNamesContainerColor = Color(0xFF8B0000) +val ClearPlayersTextFieldContainerColor = Color(0xFF8B0000) +val SavePlayersButtonContainerColor = Color(0xFF006400) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 956ecff..464591c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,7 +24,7 @@ Esconder jogadores ícone do botão para esconder jogadores Mostrar jogadores - Jogadores + Jogadores (%d) Editar Pausar Continuar @@ -32,4 +32,14 @@ botão para iniciar o cronômetro botão para reiniciar cronômetro botão para pausar o cronômetro + Nível: + diminuir nível + aumentar nível + Times sorteados + Para marcar um jogador como goleiro basta inserir (g) após o seu nome + Entendi + Adicionar Goleiro + mostrar dica para adicionar goleiro + goal keeper icon + not goal keeper icon \ No newline at end of file diff --git a/app/src/test/java/br/com/alexf/boraprofut/TeamDrawerUseCaseTest.kt b/app/src/test/java/br/com/alexf/boraprofut/TeamDrawerUseCaseTest.kt index b3f75d7..bd0c301 100644 --- a/app/src/test/java/br/com/alexf/boraprofut/TeamDrawerUseCaseTest.kt +++ b/app/src/test/java/br/com/alexf/boraprofut/TeamDrawerUseCaseTest.kt @@ -2,12 +2,17 @@ package br.com.alexf.boraprofut import br.com.alexf.boraprofut.features.drawTeams.useCases.TeamDrawerUseCase import br.com.alexf.boraprofut.models.Player +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldContain +import org.amshove.kluent.shouldContainAny +import org.amshove.kluent.shouldNotBeEqualTo import org.amshove.kluent.shouldNotBeGreaterThan import org.amshove.kluent.shouldNotContainAny -import org.junit.Assert import org.junit.Test import kotlin.random.Random +private const val GOAL_KEEPER = "Goalkeeper" + class TeamDrawerUseCaseTest { private val teamDrawer = TeamDrawerUseCase() @@ -16,7 +21,7 @@ class TeamDrawerUseCaseTest { fun shouldDrawTeamPlayersRandomlyGivenAmount() { val players = generatePlayers(12) val teams = teamDrawer.drawRandomTeams(players, 4) - Assert.assertEquals(teams.size, 3) + teams.size shouldBeEqualTo 3 teams.forEach { team -> teams.filter { it != team @@ -34,14 +39,78 @@ class TeamDrawerUseCaseTest { } teamAverages.forEach { average -> teamAverages.forEach { - (it - average).shouldNotBeGreaterThan(1) + (it - average) shouldNotBeGreaterThan 2 + } + } + } + + @Test + fun shouldContainGoalKeeperInAllTeams() { + val players = generatePlayers(8).toMutableSet() + val playersPerTeam = 5 + players.addAll( + setOf( + Player("$GOAL_KEEPER 1", isGoalKeeper = true), + Player("$GOAL_KEEPER 2", isGoalKeeper = true), + ) + ) + val drawnTeams = teamDrawer.drawRandomTeams(players = players, playersPerTeam = playersPerTeam) + drawnTeams.size shouldBeEqualTo 2 + drawnTeams.forEach { team -> + team shouldContainAny { + it.isGoalKeeper } + team.size shouldBeEqualTo playersPerTeam } } + @Test + fun shouldNotContainGoalKeeperInAllTeams() { + val players = generatePlayers(10) + val playersPerTeam = 5 + val drawnTeams = teamDrawer.drawRandomTeams(players = players, playersPerTeam = playersPerTeam) + drawnTeams.size shouldBeEqualTo 2 + drawnTeams.forEach { team -> + team shouldNotContainAny { + it.isGoalKeeper + } + team.size shouldBeEqualTo playersPerTeam + } + } + + @Test + fun shouldContainGoalKeeperJustInTheFirstTeam() { + val players = generatePlayers(9).toMutableSet() + val playersPerTeam = 5 + val goalKeeper = Player(GOAL_KEEPER, isGoalKeeper = true) + players.add(goalKeeper) + val drawnTeams = teamDrawer.drawRandomTeams(players = players, playersPerTeam = playersPerTeam) + drawnTeams.size shouldBeEqualTo 2 + drawnTeams[0] shouldContain goalKeeper + drawnTeams[1] shouldNotContainAny { + it.isGoalKeeper + } + drawnTeams.forEach {team -> + team.size shouldBeEqualTo playersPerTeam + } + } + + @Test + fun shouldContainATeamWithoutASufficientNumberOfPlayers() { + var players = generatePlayers(9) + val playersPerTeam = 4 + var drawnTeams = + teamDrawer.drawRandomTeams(players = players, playersPerTeam = playersPerTeam) + drawnTeams.last().size shouldNotBeEqualTo playersPerTeam + players = generatePlayers(11) + drawnTeams = + teamDrawer.drawBalancedTeams(players = players, playersPerTeam = playersPerTeam) + drawnTeams.last().size shouldNotBeEqualTo playersPerTeam + } + } private fun generatePlayers(amount: Int): Set = List(amount) { - Player(name = "jogador ${it + 1}", level = Random.nextInt(1, 10)) + Player(name = "player ${it + 1}", level = Random.nextInt(1, 10)) }.toSet()