Compare commits

..

No commits in common. "main" and "v1.0-16" have entirely different histories.

8 changed files with 68 additions and 313 deletions

View file

@ -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")

View file

@ -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)
) )
) )

View file

@ -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>>

View file

@ -18,9 +18,6 @@ class EntryFragment : Fragment() {
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,
container: ViewGroup?, container: ViewGroup?,
@ -37,71 +34,9 @@ class EntryFragment : Fragment() {
setupRecyclerView() setupRecyclerView()
setupSubmitButton() setupSubmitButton()
setupAutoAnalysis() setupAutoAnalysis()
}
override fun onResume() { // Check if we should show conversions on startup
super.onResume() viewModel.checkConversionCriteria()
// 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
val score = viewModel.lastSubmittedScore.value
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
}
// Observe last submitted score value viewModel.lastSubmittedScore.value?.let { score ->
viewModel.lastSubmittedScore.observe(viewLifecycleOwner) { _ -> binding.textViewOriginalScore.text = "Your $game score of ${"%,d".format(score)} is equivalent to:"
// Update the analysis card when score changes binding.cardAnalysisResults.visibility = View.VISIBLE
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)
} }

View file

@ -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
@ -46,17 +43,6 @@ class EntryViewModel(application: Application) : AndroidViewModel(application) {
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() {
viewModelScope.launch {
val scoreCount = totalScoreCount.value ?: 0 val scoreCount = totalScoreCount.value ?: 0
val gameCount = gamesWithScores.value?.size ?: 0 val gameCount = gamesWithScores.value?.size ?: 0
// Only show conversions if there are at least 3 scores across at least 2 games // Only show conversions if there are at least 3 scores across at least 2 games
val shouldShow = scoreCount >= 3 && gameCount >= 2 _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 {

View file

@ -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()
} }
@ -64,64 +53,6 @@ class StatsFragment : Fragment() {
} }
} }
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 ->
scoreAdapter.submitList(scores) scoreAdapter.submitList(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() {

View file

@ -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)
} }

View file

@ -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"