mirror of
https://github.com/cmclark00/TetriStats.git
synced 2025-05-18 15:15:21 +01:00
Compare commits
No commits in common. "main" and "v1.0-16" have entirely different histories.
8 changed files with 68 additions and 313 deletions
|
@ -83,9 +83,6 @@ dependencies {
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
|
||||||
|
|
||||||
// MPAndroidChart for progress visualization
|
|
||||||
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
|
|
||||||
|
|
||||||
// Room
|
// Room
|
||||||
implementation("androidx.room:room-runtime:2.6.1")
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
implementation("androidx.room:room-ktx:2.6.1")
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
|
|
|
@ -16,9 +16,7 @@ object ScalingFactors {
|
||||||
"Tetris DS" to RangeScalingFactor(3.0, 3.3, 4.5),
|
"Tetris DS" to RangeScalingFactor(3.0, 3.3, 4.5),
|
||||||
"Tetris Effect" to RangeScalingFactor(2.5, 3.8, 4.5),
|
"Tetris Effect" to RangeScalingFactor(2.5, 3.8, 4.5),
|
||||||
"Rosy Retrospection DX" to RangeScalingFactor(4.0, 1.5, 1.8),
|
"Rosy Retrospection DX" to RangeScalingFactor(4.0, 1.5, 1.8),
|
||||||
"Apotris" to RangeScalingFactor(1.8, 3.8, 4.4),
|
"Apotris" to RangeScalingFactor(1.8, 3.8, 4.4)
|
||||||
"Modretro Tetris" to RangeScalingFactor(2.0, 2.5, 3.0),
|
|
||||||
"Tetris Mobile" to RangeScalingFactor(2.2, 2.8, 3.5)
|
|
||||||
),
|
),
|
||||||
"Game Boy Tetris" to mapOf(
|
"Game Boy Tetris" to mapOf(
|
||||||
"NES Tetris" to 1.33,
|
"NES Tetris" to 1.33,
|
||||||
|
@ -26,9 +24,7 @@ object ScalingFactors {
|
||||||
"Tetris DS" to RangeScalingFactor(4.0, 2.0, 2.0),
|
"Tetris DS" to RangeScalingFactor(4.0, 2.0, 2.0),
|
||||||
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 2.3),
|
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 2.3),
|
||||||
"Rosy Retrospection DX" to 1.1,
|
"Rosy Retrospection DX" to 1.1,
|
||||||
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33),
|
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33)
|
||||||
"Modretro Tetris" to RangeScalingFactor(1.5, 1.8, 2.0),
|
|
||||||
"Tetris Mobile" to RangeScalingFactor(1.6, 1.9, 2.1)
|
|
||||||
),
|
),
|
||||||
"Tetris DX" to mapOf(
|
"Tetris DX" to mapOf(
|
||||||
"NES Tetris" to 1.33,
|
"NES Tetris" to 1.33,
|
||||||
|
@ -36,9 +32,7 @@ object ScalingFactors {
|
||||||
"Tetris DS" to RangeScalingFactor(4.0, 2.0, 2.0),
|
"Tetris DS" to RangeScalingFactor(4.0, 2.0, 2.0),
|
||||||
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 2.3),
|
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 2.3),
|
||||||
"Rosy Retrospection DX" to 1.1,
|
"Rosy Retrospection DX" to 1.1,
|
||||||
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33),
|
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33)
|
||||||
"Modretro Tetris" to RangeScalingFactor(1.5, 1.8, 2.0),
|
|
||||||
"Tetris Mobile" to RangeScalingFactor(1.6, 1.9, 2.1)
|
|
||||||
),
|
),
|
||||||
"Tetris DS" to mapOf(
|
"Tetris DS" to mapOf(
|
||||||
"NES Tetris" to RangeScalingFactor(0.33, 0.3, 0.22),
|
"NES Tetris" to RangeScalingFactor(0.33, 0.3, 0.22),
|
||||||
|
@ -46,9 +40,7 @@ object ScalingFactors {
|
||||||
"Tetris DX" to RangeScalingFactor(0.25, 0.5, 0.5),
|
"Tetris DX" to RangeScalingFactor(0.25, 0.5, 0.5),
|
||||||
"Tetris Effect" to RangeScalingFactor(0.83, 0.91, 1.0),
|
"Tetris Effect" to RangeScalingFactor(0.83, 0.91, 1.0),
|
||||||
"Rosy Retrospection DX" to RangeScalingFactor(0.25, 0.91, 0.67),
|
"Rosy Retrospection DX" to RangeScalingFactor(0.25, 0.91, 0.67),
|
||||||
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.9),
|
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.9)
|
||||||
"Modretro Tetris" to RangeScalingFactor(0.4, 0.5, 0.6),
|
|
||||||
"Tetris Mobile" to RangeScalingFactor(0.45, 0.55, 0.65)
|
|
||||||
),
|
),
|
||||||
"Tetris Effect" to mapOf(
|
"Tetris Effect" to mapOf(
|
||||||
"NES Tetris" to RangeScalingFactor(0.4, 0.26, 0.22),
|
"NES Tetris" to RangeScalingFactor(0.4, 0.26, 0.22),
|
||||||
|
@ -56,9 +48,7 @@ object ScalingFactors {
|
||||||
"Tetris DX" to RangeScalingFactor(0.25, 0.43, 0.43),
|
"Tetris DX" to RangeScalingFactor(0.25, 0.43, 0.43),
|
||||||
"Tetris DS" to RangeScalingFactor(1.2, 1.1, 1.0),
|
"Tetris DS" to RangeScalingFactor(1.2, 1.1, 1.0),
|
||||||
"Rosy Retrospection DX" to RangeScalingFactor(0.25, 0.43, 0.57),
|
"Rosy Retrospection DX" to RangeScalingFactor(0.25, 0.43, 0.57),
|
||||||
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.85),
|
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.85)
|
||||||
"Modretro Tetris" to RangeScalingFactor(0.45, 0.55, 0.65),
|
|
||||||
"Tetris Mobile" to RangeScalingFactor(0.5, 0.6, 0.7)
|
|
||||||
),
|
),
|
||||||
"Rosy Retrospection DX" to mapOf(
|
"Rosy Retrospection DX" to mapOf(
|
||||||
"NES Tetris" to RangeScalingFactor(0.25, 0.67, 0.57),
|
"NES Tetris" to RangeScalingFactor(0.25, 0.67, 0.57),
|
||||||
|
@ -66,9 +56,7 @@ object ScalingFactors {
|
||||||
"Tetris DX" to 0.91,
|
"Tetris DX" to 0.91,
|
||||||
"Tetris DS" to RangeScalingFactor(4.0, 1.5, 1.8),
|
"Tetris DS" to RangeScalingFactor(4.0, 1.5, 1.8),
|
||||||
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 1.8),
|
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 1.8),
|
||||||
"Apotris" to RangeScalingFactor(1.1, 0.67, 0.5),
|
"Apotris" to RangeScalingFactor(1.1, 0.67, 0.5)
|
||||||
"Modretro Tetris" to RangeScalingFactor(1.3, 1.5, 1.7),
|
|
||||||
"Tetris Mobile" to RangeScalingFactor(1.4, 1.6, 1.8)
|
|
||||||
),
|
),
|
||||||
"Apotris" to mapOf(
|
"Apotris" to mapOf(
|
||||||
"NES Tetris" to RangeScalingFactor(0.56, 0.26, 0.23),
|
"NES Tetris" to RangeScalingFactor(0.56, 0.26, 0.23),
|
||||||
|
@ -76,29 +64,7 @@ object ScalingFactors {
|
||||||
"Tetris DX" to RangeScalingFactor(0.75, 0.75, 0.5),
|
"Tetris DX" to RangeScalingFactor(0.75, 0.75, 0.5),
|
||||||
"Tetris DS" to RangeScalingFactor(3.0, 1.5, 1.0),
|
"Tetris DS" to RangeScalingFactor(3.0, 1.5, 1.0),
|
||||||
"Tetris Effect" to RangeScalingFactor(3.0, 1.7, 1.2),
|
"Tetris Effect" to RangeScalingFactor(3.0, 1.7, 1.2),
|
||||||
"Rosy Retrospection DX" to RangeScalingFactor(1.1, 0.67, 0.5),
|
"Rosy Retrospection DX" to RangeScalingFactor(1.1, 0.67, 0.5)
|
||||||
"Modretro Tetris" to RangeScalingFactor(1.2, 0.9, 0.7),
|
|
||||||
"Tetris Mobile" to RangeScalingFactor(1.3, 1.0, 0.8)
|
|
||||||
),
|
|
||||||
"Modretro Tetris" to mapOf(
|
|
||||||
"NES Tetris" to RangeScalingFactor(0.5, 0.4, 0.33),
|
|
||||||
"Game Boy Tetris" to RangeScalingFactor(0.67, 0.56, 0.5),
|
|
||||||
"Tetris DX" to RangeScalingFactor(0.67, 0.56, 0.5),
|
|
||||||
"Tetris DS" to RangeScalingFactor(2.5, 2.0, 1.67),
|
|
||||||
"Tetris Effect" to RangeScalingFactor(2.22, 1.82, 1.54),
|
|
||||||
"Rosy Retrospection DX" to RangeScalingFactor(0.77, 0.67, 0.59),
|
|
||||||
"Apotris" to RangeScalingFactor(0.83, 1.11, 1.43),
|
|
||||||
"Tetris Mobile" to RangeScalingFactor(1.1, 1.1, 1.1)
|
|
||||||
),
|
|
||||||
"Tetris Mobile" to mapOf(
|
|
||||||
"NES Tetris" to RangeScalingFactor(0.45, 0.36, 0.29),
|
|
||||||
"Game Boy Tetris" to RangeScalingFactor(0.63, 0.53, 0.48),
|
|
||||||
"Tetris DX" to RangeScalingFactor(0.63, 0.53, 0.48),
|
|
||||||
"Tetris DS" to RangeScalingFactor(2.22, 1.82, 1.54),
|
|
||||||
"Tetris Effect" to RangeScalingFactor(2.0, 1.67, 1.43),
|
|
||||||
"Rosy Retrospection DX" to RangeScalingFactor(0.71, 0.63, 0.56),
|
|
||||||
"Apotris" to RangeScalingFactor(0.77, 1.0, 1.25),
|
|
||||||
"Modretro Tetris" to RangeScalingFactor(0.91, 0.91, 0.91)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,6 @@ interface ScoreDao {
|
||||||
@Query("SELECT * FROM scores WHERE gameVersion = :gameVersion")
|
@Query("SELECT * FROM scores WHERE gameVersion = :gameVersion")
|
||||||
fun getScoresForGame(gameVersion: String): LiveData<List<Score>>
|
fun getScoresForGame(gameVersion: String): LiveData<List<Score>>
|
||||||
|
|
||||||
@Query("SELECT * FROM scores WHERE gameVersion = :gameVersion ORDER BY dateRecorded ASC")
|
|
||||||
fun getScoresForGameByDate(gameVersion: String): LiveData<List<Score>>
|
|
||||||
|
|
||||||
@Query("SELECT DISTINCT gameVersion FROM scores")
|
@Query("SELECT DISTINCT gameVersion FROM scores")
|
||||||
fun getGamesWithScores(): LiveData<List<String>>
|
fun getGamesWithScores(): LiveData<List<String>>
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,6 @@ class EntryFragment : Fragment() {
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private val viewModel: EntryViewModel by viewModels { EntryViewModelFactory(requireActivity().application) }
|
private val viewModel: EntryViewModel by viewModels { EntryViewModelFactory(requireActivity().application) }
|
||||||
private lateinit var equivalentScoreAdapter: EquivalentScoreAdapter
|
private lateinit var equivalentScoreAdapter: EquivalentScoreAdapter
|
||||||
|
|
||||||
// Flag to track if we already showed the requirements toast
|
|
||||||
private var hasShownRequirementsToast = false
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -37,71 +34,9 @@ class EntryFragment : Fragment() {
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupSubmitButton()
|
setupSubmitButton()
|
||||||
setupAutoAnalysis()
|
setupAutoAnalysis()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
// Refresh conversions when returning to this fragment
|
|
||||||
refreshConversions()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force refresh the conversions using the last submitted values
|
|
||||||
*/
|
|
||||||
private fun refreshConversions() {
|
|
||||||
if (viewModel.showConversion.value == true) {
|
|
||||||
// If we have last submitted values, regenerate conversions
|
|
||||||
val game = viewModel.lastSubmittedGame.value
|
|
||||||
val score = viewModel.lastSubmittedScore.value
|
|
||||||
|
|
||||||
if (game != null && score != null) {
|
|
||||||
viewModel.refreshEquivalentScores(game, score)
|
|
||||||
|
|
||||||
// Make sure UI updates immediately by forcing an adapter refresh
|
|
||||||
viewModel.equivalentScores.value?.let { scores ->
|
|
||||||
equivalentScoreAdapter.submitList(null) // Clear first
|
|
||||||
equivalentScoreAdapter.submitList(scores) // Then add new list
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure card is visible
|
|
||||||
updateAnalysisCard()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the analysis card visibility and contents based on current state
|
|
||||||
*/
|
|
||||||
private fun updateAnalysisCard() {
|
|
||||||
if (viewModel.showConversion.value != true) {
|
|
||||||
binding.cardAnalysisResults.visibility = View.GONE
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val game = viewModel.lastSubmittedGame.value
|
// Check if we should show conversions on startup
|
||||||
val score = viewModel.lastSubmittedScore.value
|
viewModel.checkConversionCriteria()
|
||||||
|
|
||||||
if (game != null && score != null) {
|
|
||||||
// Get the list of games with scores
|
|
||||||
val playedGames = viewModel.gamesWithScores.value ?: listOf()
|
|
||||||
|
|
||||||
// Make sure we don't show the source game in the equivalent dropdown
|
|
||||||
val filteredGames = playedGames.filter { it != game }
|
|
||||||
if (filteredGames.isNotEmpty()) {
|
|
||||||
binding.textViewOriginalScore.text = "Your $game score of ${"%,d".format(score)} is equivalent to:"
|
|
||||||
binding.cardAnalysisResults.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
val filteredAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, filteredGames)
|
|
||||||
binding.autoCompleteEquivalentGame.setAdapter(filteredAdapter)
|
|
||||||
|
|
||||||
// Select first game by default
|
|
||||||
binding.autoCompleteEquivalentGame.setText(filteredGames[0], false)
|
|
||||||
viewModel.setSelectedEquivalentGame(filteredGames[0])
|
|
||||||
} else {
|
|
||||||
// If no other games to convert to, hide the card
|
|
||||||
binding.cardAnalysisResults.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupGameVersionDropdown() {
|
private fun setupGameVersionDropdown() {
|
||||||
|
@ -112,9 +47,7 @@ class EntryFragment : Fragment() {
|
||||||
"Tetris DS",
|
"Tetris DS",
|
||||||
"Tetris Effect",
|
"Tetris Effect",
|
||||||
"Rosy Retrospection DX",
|
"Rosy Retrospection DX",
|
||||||
"Apotris",
|
"Apotris"
|
||||||
"Modretro Tetris",
|
|
||||||
"Tetris Mobile"
|
|
||||||
)
|
)
|
||||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
|
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
|
||||||
binding.autoCompleteGameVersion.setAdapter(adapter)
|
binding.autoCompleteGameVersion.setAdapter(adapter)
|
||||||
|
@ -134,12 +67,18 @@ class EntryFragment : Fragment() {
|
||||||
|
|
||||||
// Observe if we should show conversions
|
// Observe if we should show conversions
|
||||||
viewModel.showConversion.observe(viewLifecycleOwner) { shouldShow ->
|
viewModel.showConversion.observe(viewLifecycleOwner) { shouldShow ->
|
||||||
// No need to show toast here - we'll do it only after score submission
|
if (!shouldShow) {
|
||||||
if (shouldShow) {
|
// Hide analysis card if we don't meet criteria
|
||||||
// Update card when showConversion changes
|
|
||||||
updateAnalysisCard()
|
|
||||||
} else {
|
|
||||||
binding.cardAnalysisResults.visibility = View.GONE
|
binding.cardAnalysisResults.visibility = View.GONE
|
||||||
|
|
||||||
|
// Also show a message that not enough scores have been entered
|
||||||
|
if (viewModel.lastSubmittedGame.value != null) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Enter at least 3 scores across 2 different games to see conversions",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,8 +86,8 @@ class EntryFragment : Fragment() {
|
||||||
viewModel.gamesWithScores.observe(viewLifecycleOwner) { games ->
|
viewModel.gamesWithScores.observe(viewLifecycleOwner) { games ->
|
||||||
// Setup the game dropdown for adding equivalents - only with played games
|
// Setup the game dropdown for adding equivalents - only with played games
|
||||||
if (games.isNotEmpty()) {
|
if (games.isNotEmpty()) {
|
||||||
// Update card when games list changes
|
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
|
||||||
updateAnalysisCard()
|
binding.autoCompleteEquivalentGame.setAdapter(adapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,22 +110,41 @@ class EntryFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe last submitted score details
|
// Observe last submitted score details
|
||||||
viewModel.lastSubmittedGame.observe(viewLifecycleOwner) { _ ->
|
viewModel.lastSubmittedGame.observe(viewLifecycleOwner) { game ->
|
||||||
// Update the analysis card when last submitted game changes
|
// Only continue if showConversion is true
|
||||||
updateAnalysisCard()
|
if (viewModel.showConversion.value != true) return@observe
|
||||||
}
|
|
||||||
|
viewModel.lastSubmittedScore.value?.let { score ->
|
||||||
// Observe last submitted score value
|
binding.textViewOriginalScore.text = "Your $game score of ${"%,d".format(score)} is equivalent to:"
|
||||||
viewModel.lastSubmittedScore.observe(viewLifecycleOwner) { _ ->
|
binding.cardAnalysisResults.visibility = View.VISIBLE
|
||||||
// Update the analysis card when score changes
|
|
||||||
updateAnalysisCard()
|
// Get the list of games with scores
|
||||||
|
val playedGames = viewModel.gamesWithScores.value ?: listOf()
|
||||||
|
|
||||||
|
// Make sure we don't show the source game in the equivalent dropdown
|
||||||
|
val filteredGames = playedGames.filter { it != game }
|
||||||
|
if (filteredGames.isNotEmpty()) {
|
||||||
|
val filteredAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, filteredGames)
|
||||||
|
binding.autoCompleteEquivalentGame.setAdapter(filteredAdapter)
|
||||||
|
|
||||||
|
// Select first game by default
|
||||||
|
binding.autoCompleteEquivalentGame.setText(filteredGames[0], false)
|
||||||
|
viewModel.setSelectedEquivalentGame(filteredGames[0])
|
||||||
|
} else {
|
||||||
|
// If no other games to convert to, hide the card
|
||||||
|
binding.cardAnalysisResults.visibility = View.GONE
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Add scores for other games to see conversions",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Observe equivalent scores
|
// Observe equivalent scores
|
||||||
viewModel.equivalentScores.observe(viewLifecycleOwner) { scores ->
|
viewModel.equivalentScores.observe(viewLifecycleOwner) { scores ->
|
||||||
if (scores.isNotEmpty()) {
|
if (scores.isNotEmpty()) {
|
||||||
// Force a clean update by clearing first
|
|
||||||
equivalentScoreAdapter.submitList(null)
|
|
||||||
equivalentScoreAdapter.submitList(scores)
|
equivalentScoreAdapter.submitList(scores)
|
||||||
} else if (viewModel.showConversion.value == true) {
|
} else if (viewModel.showConversion.value == true) {
|
||||||
// If we should be showing conversions but have no scores, probably no other games
|
// If we should be showing conversions but have no scores, probably no other games
|
||||||
|
@ -213,17 +171,8 @@ class EntryFragment : Fragment() {
|
||||||
)
|
)
|
||||||
clearInputs()
|
clearInputs()
|
||||||
|
|
||||||
// Force immediate refresh of conversions
|
// Only scroll down if we're going to show conversions
|
||||||
if (viewModel.showConversion.value == false) {
|
if (viewModel.showConversion.value == true) {
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
"Enter at least 3 scores across 2 different games to see conversions",
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
} else {
|
|
||||||
refreshConversions()
|
|
||||||
|
|
||||||
// Scroll down to show the analysis results
|
|
||||||
binding.root.post {
|
binding.root.post {
|
||||||
binding.root.fullScroll(View.FOCUS_DOWN)
|
binding.root.fullScroll(View.FOCUS_DOWN)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.accidentalproductions.tetristats.ui.entry
|
package com.accidentalproductions.tetristats.ui.entry
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import com.accidentalproductions.tetristats.TetriStatsApplication
|
import com.accidentalproductions.tetristats.TetriStatsApplication
|
||||||
import com.accidentalproductions.tetristats.data.Score
|
import com.accidentalproductions.tetristats.data.Score
|
||||||
|
@ -23,9 +22,7 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
"Tetris DS",
|
"Tetris DS",
|
||||||
"Tetris Effect",
|
"Tetris Effect",
|
||||||
"Rosy Retrospection DX",
|
"Rosy Retrospection DX",
|
||||||
"Apotris",
|
"Apotris"
|
||||||
"Modretro Tetris",
|
|
||||||
"Tetris Mobile"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Track user played games and score counts
|
// Track user played games and score counts
|
||||||
|
@ -45,17 +42,6 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
val lastSubmittedGame: LiveData<String> = _lastSubmittedGame
|
val lastSubmittedGame: LiveData<String> = _lastSubmittedGame
|
||||||
val lastSubmittedScore: LiveData<Int> = _lastSubmittedScore
|
val lastSubmittedScore: LiveData<Int> = _lastSubmittedScore
|
||||||
val showConversion: LiveData<Boolean> = _showConversion
|
val showConversion: LiveData<Boolean> = _showConversion
|
||||||
|
|
||||||
init {
|
|
||||||
// Set up observers to update conversion criteria whenever relevant data changes
|
|
||||||
gamesWithScores.observeForever {
|
|
||||||
checkConversionCriteria()
|
|
||||||
}
|
|
||||||
|
|
||||||
totalScoreCount.observeForever {
|
|
||||||
checkConversionCriteria()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getScoresForGame(gameVersion: String): LiveData<List<Score>> {
|
fun getScoresForGame(gameVersion: String): LiveData<List<Score>> {
|
||||||
return scoreDao.getScoresForGame(gameVersion)
|
return scoreDao.getScoresForGame(gameVersion)
|
||||||
|
@ -69,26 +55,12 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
* Check if we should show conversions based on score count and game count
|
* Check if we should show conversions based on score count and game count
|
||||||
*/
|
*/
|
||||||
fun checkConversionCriteria() {
|
fun checkConversionCriteria() {
|
||||||
val scoreCount = totalScoreCount.value ?: 0
|
viewModelScope.launch {
|
||||||
val gameCount = gamesWithScores.value?.size ?: 0
|
val scoreCount = totalScoreCount.value ?: 0
|
||||||
|
val gameCount = gamesWithScores.value?.size ?: 0
|
||||||
// Only show conversions if there are at least 3 scores across at least 2 games
|
|
||||||
val shouldShow = scoreCount >= 3 && gameCount >= 2
|
// Only show conversions if there are at least 3 scores across at least 2 games
|
||||||
|
_showConversion.postValue(scoreCount >= 3 && gameCount >= 2)
|
||||||
// For debugging
|
|
||||||
Log.d("TetriStats", "Checking conversion criteria: scores=$scoreCount, games=$gameCount, shouldShow=$shouldShow")
|
|
||||||
|
|
||||||
_showConversion.postValue(shouldShow)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force refresh the equivalent scores - use this to ensure UI has latest values
|
|
||||||
*/
|
|
||||||
fun refreshEquivalentScores(fromGame: String, score: Int) {
|
|
||||||
// Only refresh if conversions should be showing
|
|
||||||
if (_showConversion.value == true) {
|
|
||||||
Log.d("TetriStats", "Refreshing equivalent scores for $fromGame score $score")
|
|
||||||
generateEquivalentScores(fromGame, score)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,14 +82,14 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
scoreDao.insert(newScore)
|
scoreDao.insert(newScore)
|
||||||
|
|
||||||
// After inserting, update the last submitted values
|
// After inserting, update the last submitted values
|
||||||
_lastSubmittedGame.value = gameVersion // Use immediate value change instead of postValue
|
_lastSubmittedGame.postValue(gameVersion)
|
||||||
_lastSubmittedScore.value = score // Use immediate value change instead of postValue
|
_lastSubmittedScore.postValue(score)
|
||||||
|
|
||||||
// Immediately check conversion criteria with current values
|
// Check if we should show conversions
|
||||||
checkConversionCriteria()
|
checkConversionCriteria()
|
||||||
|
|
||||||
// Immediate refresh regardless if we just reached the criteria threshold
|
// Only generate equivalent scores if we meet the criteria
|
||||||
if (totalScoreCount.value ?: 0 >= 3 && (gamesWithScores.value?.size ?: 0) >= 2) {
|
if (_showConversion.value == true) {
|
||||||
generateEquivalentScores(gameVersion, score)
|
generateEquivalentScores(gameVersion, score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,8 +121,7 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use setValue for immediate update on main thread rather than postValue
|
_equivalentScores.postValue(equivalents)
|
||||||
_equivalentScores.value = equivalents
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,13 +141,6 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
// Remove our observers to prevent leaks
|
|
||||||
gamesWithScores.removeObserver { checkConversionCriteria() }
|
|
||||||
totalScoreCount.removeObserver { checkConversionCriteria() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntryViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
|
class EntryViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.accidentalproductions.tetristats.ui.stats
|
package com.accidentalproductions.tetristats.ui.stats
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -9,16 +8,7 @@ import android.widget.ArrayAdapter
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.accidentalproductions.tetristats.R
|
|
||||||
import com.accidentalproductions.tetristats.databinding.FragmentStatsBinding
|
import com.accidentalproductions.tetristats.databinding.FragmentStatsBinding
|
||||||
import com.github.mikephil.charting.components.XAxis
|
|
||||||
import com.github.mikephil.charting.data.Entry
|
|
||||||
import com.github.mikephil.charting.data.LineData
|
|
||||||
import com.github.mikephil.charting.data.LineDataSet
|
|
||||||
import com.github.mikephil.charting.formatter.ValueFormatter
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class StatsFragment : Fragment() {
|
class StatsFragment : Fragment() {
|
||||||
private var _binding: FragmentStatsBinding? = null
|
private var _binding: FragmentStatsBinding? = null
|
||||||
|
@ -40,7 +30,6 @@ class StatsFragment : Fragment() {
|
||||||
|
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
setupGameFilter()
|
setupGameFilter()
|
||||||
setupProgressChart()
|
|
||||||
observeStats()
|
observeStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,64 +52,6 @@ class StatsFragment : Fragment() {
|
||||||
viewModel.setSelectedGame(selectedGame)
|
viewModel.setSelectedGame(selectedGame)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupProgressChart() {
|
|
||||||
with(binding.chartProgress) {
|
|
||||||
description.isEnabled = false
|
|
||||||
legend.isEnabled = true
|
|
||||||
setTouchEnabled(true)
|
|
||||||
setDrawGridBackground(false)
|
|
||||||
isDragEnabled = true
|
|
||||||
setScaleEnabled(true)
|
|
||||||
setPinchZoom(true)
|
|
||||||
|
|
||||||
axisRight.isEnabled = false
|
|
||||||
|
|
||||||
xAxis.position = XAxis.XAxisPosition.BOTTOM
|
|
||||||
xAxis.granularity = 1f
|
|
||||||
xAxis.setDrawGridLines(false)
|
|
||||||
|
|
||||||
axisLeft.setDrawGridLines(true)
|
|
||||||
axisLeft.axisMinimum = 0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateProgressChart(scores: List<Entry>, dates: List<Long>) {
|
|
||||||
if (scores.isEmpty()) {
|
|
||||||
binding.chartProgress.clear()
|
|
||||||
binding.chartProgress.invalidate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataSet = LineDataSet(scores, "Score Progress").apply {
|
|
||||||
mode = LineDataSet.Mode.CUBIC_BEZIER
|
|
||||||
color = resources.getColor(R.color.tetris_navy, null)
|
|
||||||
lineWidth = 2f
|
|
||||||
setDrawCircles(true)
|
|
||||||
setCircleColor(resources.getColor(R.color.tetris_navy, null))
|
|
||||||
circleRadius = 4f
|
|
||||||
setDrawValues(false)
|
|
||||||
highLightColor = Color.rgb(244, 117, 117)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lineData = LineData(dataSet)
|
|
||||||
binding.chartProgress.data = lineData
|
|
||||||
|
|
||||||
// Format X-axis labels (dates)
|
|
||||||
val dateFormat = SimpleDateFormat("MM/dd", Locale.getDefault())
|
|
||||||
binding.chartProgress.xAxis.valueFormatter = object : ValueFormatter() {
|
|
||||||
override fun getFormattedValue(value: Float): String {
|
|
||||||
val index = value.toInt()
|
|
||||||
return if (index >= 0 && index < dates.size) {
|
|
||||||
dateFormat.format(Date(dates[index]))
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.chartProgress.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeStats() {
|
private fun observeStats() {
|
||||||
viewModel.filteredScores.observe(viewLifecycleOwner) { scores ->
|
viewModel.filteredScores.observe(viewLifecycleOwner) { scores ->
|
||||||
|
@ -134,24 +65,6 @@ class StatsFragment : Fragment() {
|
||||||
viewModel.highScore.observe(viewLifecycleOwner) { highScore ->
|
viewModel.highScore.observe(viewLifecycleOwner) { highScore ->
|
||||||
binding.textViewHighScore.text = "%,d".format(highScore)
|
binding.textViewHighScore.text = "%,d".format(highScore)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.scoresByDate.observe(viewLifecycleOwner) { scores ->
|
|
||||||
// Convert scores to entries for the chart
|
|
||||||
if (scores.isNotEmpty()) {
|
|
||||||
val entries = mutableListOf<Entry>()
|
|
||||||
val dates = mutableListOf<Long>()
|
|
||||||
|
|
||||||
scores.forEachIndexed { index, score ->
|
|
||||||
entries.add(Entry(index.toFloat(), score.scoreValue.toFloat()))
|
|
||||||
dates.add(score.dateRecorded)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgressChart(entries, dates)
|
|
||||||
} else {
|
|
||||||
binding.chartProgress.clear()
|
|
||||||
binding.chartProgress.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|
|
@ -18,10 +18,6 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
scoreDao.getScoresForGame(game)
|
scoreDao.getScoresForGame(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
val scoresByDate: LiveData<List<Score>> = _selectedGame.switchMap { game ->
|
|
||||||
scoreDao.getScoresForGameByDate(game)
|
|
||||||
}
|
|
||||||
|
|
||||||
val averageScore: LiveData<Double> = _selectedGame.switchMap { game ->
|
val averageScore: LiveData<Double> = _selectedGame.switchMap { game ->
|
||||||
scoreDao.getAverageScore(game)
|
scoreDao.getAverageScore(game)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,33 +31,6 @@
|
||||||
android:inputType="none"/>
|
android:inputType="none"/>
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
app:cardCornerRadius="8dp"
|
|
||||||
app:cardElevation="4dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Progress Chart"
|
|
||||||
android:textAppearance="?attr/textAppearanceSubtitle1" />
|
|
||||||
|
|
||||||
<com.github.mikephil.charting.charts.LineChart
|
|
||||||
android:id="@+id/chartProgress"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="200dp"
|
|
||||||
android:layout_marginTop="8dp" />
|
|
||||||
</LinearLayout>
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerViewScores"
|
android:id="@+id/recyclerViewScores"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue