Initial commit: TetriStats Application with score tracking, conversion, and statistics for multiple Tetris games

This commit is contained in:
cmclark00 2025-03-20 01:14:21 -04:00
commit ca8b4fd77b
78 changed files with 2587 additions and 0 deletions

8
app/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
/build
/captures
.externalNativeBuild
.cxx
local.properties
# Keep Room schema files
!schemas/

64
app/build.gradle.kts Normal file
View file

@ -0,0 +1,64 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
}
android {
namespace = "com.accidentalproductions.tetristats"
compileSdk = 34
defaultConfig {
applicationId = "com.accidentalproductions.tetristats"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,70 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "9628d8b2a4ce652bff86d922e43b1479",
"entities": [
{
"tableName": "scores",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `gameVersion` TEXT NOT NULL, `scoreValue` INTEGER NOT NULL, `startLevel` INTEGER, `endLevel` INTEGER, `linesCleared` INTEGER, `dateRecorded` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "gameVersion",
"columnName": "gameVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scoreValue",
"columnName": "scoreValue",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startLevel",
"columnName": "startLevel",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "endLevel",
"columnName": "endLevel",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "linesCleared",
"columnName": "linesCleared",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "dateRecorded",
"columnName": "dateRecorded",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"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, '9628d8b2a4ce652bff86d922e43b1479')"
]
}
}

View file

@ -0,0 +1,24 @@
package com.accidentalproductions.tetristats
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.accidentalproductions.tetristats", appContext.packageName)
}
}

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".TetriStatsApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TetriStats"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,37 @@
package com.accidentalproductions.tetristats
import android.os.Bundle
import com.google.android.material.bottomnavigation.BottomNavigationView
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.accidentalproductions.tetristats.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navView: BottomNavigationView = binding.navView
val navController = findNavController(R.id.nav_host_fragment_activity_main)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_entry,
R.id.navigation_history,
R.id.navigation_stats
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
}

View file

@ -0,0 +1,19 @@
package com.accidentalproductions.tetristats
import android.app.Application
import androidx.room.Room
import com.accidentalproductions.tetristats.data.ScoreDatabase
class TetriStatsApplication : Application() {
lateinit var database: ScoreDatabase
private set
override fun onCreate() {
super.onCreate()
database = Room.databaseBuilder(
applicationContext,
ScoreDatabase::class.java,
"score_database"
).build()
}
}

View file

@ -0,0 +1,90 @@
package com.accidentalproductions.tetristats.data
data class RangeScalingFactor(
val low: Double,
val mid: Double,
val high: Double
)
typealias ScalingFactor = Any // Can be Double or RangeScalingFactor
object ScalingFactors {
val FACTORS = mapOf(
"NES Tetris" to mapOf(
"Game Boy Tetris" to 0.75,
"Tetris DX" to 0.75,
"Tetris DS" to RangeScalingFactor(3.0, 3.3, 4.5),
"Tetris Effect" to RangeScalingFactor(2.5, 3.8, 4.5),
"Rosy Retrospection DX" to RangeScalingFactor(4.0, 1.5, 1.8),
"Apotris" to RangeScalingFactor(1.8, 3.8, 4.4)
),
"Game Boy Tetris" to mapOf(
"NES Tetris" to 1.33,
"Tetris DX" to 1.1,
"Tetris DS" to RangeScalingFactor(4.0, 2.0, 2.0),
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 2.3),
"Rosy Retrospection DX" to 1.1,
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33)
),
"Tetris DX" to mapOf(
"NES Tetris" to 1.33,
"Game Boy Tetris" to 0.91,
"Tetris DS" to RangeScalingFactor(4.0, 2.0, 2.0),
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 2.3),
"Rosy Retrospection DX" to 1.1,
"Apotris" to RangeScalingFactor(1.33, 1.33, 2.33)
),
"Tetris DS" to mapOf(
"NES Tetris" to RangeScalingFactor(0.33, 0.3, 0.22),
"Game Boy Tetris" 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),
"Rosy Retrospection DX" to RangeScalingFactor(0.25, 0.91, 0.67),
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.9)
),
"Tetris Effect" to mapOf(
"NES Tetris" to RangeScalingFactor(0.4, 0.26, 0.22),
"Game Boy Tetris" 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),
"Rosy Retrospection DX" to RangeScalingFactor(0.25, 0.43, 0.57),
"Apotris" to RangeScalingFactor(0.33, 0.67, 0.85)
),
"Rosy Retrospection DX" to mapOf(
"NES Tetris" to RangeScalingFactor(0.25, 0.67, 0.57),
"Game Boy Tetris" to 0.91,
"Tetris DX" to 0.91,
"Tetris DS" to RangeScalingFactor(4.0, 1.5, 1.8),
"Tetris Effect" to RangeScalingFactor(4.0, 2.3, 1.8),
"Apotris" to RangeScalingFactor(1.1, 0.67, 0.5)
),
"Apotris" to mapOf(
"NES Tetris" to RangeScalingFactor(0.56, 0.26, 0.23),
"Game Boy Tetris" 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 Effect" to RangeScalingFactor(3.0, 1.7, 1.2),
"Rosy Retrospection DX" to RangeScalingFactor(1.1, 0.67, 0.5)
)
)
fun getScalingFactor(fromGame: String, toGame: String, score: Int): Double {
val factor = FACTORS[fromGame]?.get(toGame) ?: return 1.0
return when (factor) {
is Double -> factor
is RangeScalingFactor -> {
when {
score < 100000 -> factor.low
score < 500000 -> factor.mid
else -> factor.high
}
}
else -> 1.0
}
}
fun convertScore(fromGame: String, toGame: String, score: Int): Int {
val factor = getScalingFactor(fromGame, toGame, score)
return (score * factor).toInt()
}
}

View file

@ -0,0 +1,16 @@
package com.accidentalproductions.tetristats.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "scores")
data class Score(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val gameVersion: String,
val scoreValue: Int,
val startLevel: Int? = null,
val endLevel: Int? = null,
val linesCleared: Int? = null,
val dateRecorded: Long = System.currentTimeMillis()
)

View file

@ -0,0 +1,32 @@
package com.accidentalproductions.tetristats.data
import androidx.lifecycle.LiveData
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface ScoreDao {
@Query("SELECT * FROM scores")
fun getAllScores(): LiveData<List<Score>>
@Query("SELECT * FROM scores WHERE gameVersion = :gameVersion")
fun getScoresForGame(gameVersion: String): LiveData<List<Score>>
@Query("SELECT DISTINCT gameVersion FROM scores")
fun getGamesWithScores(): LiveData<List<String>>
@Query("SELECT AVG(scoreValue) FROM scores WHERE gameVersion = :gameVersion")
fun getAverageScore(gameVersion: String): LiveData<Double>
@Query("SELECT MAX(scoreValue) FROM scores WHERE gameVersion = :gameVersion")
fun getHighScore(gameVersion: String): LiveData<Int>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(score: Score)
@Delete
suspend fun delete(score: Score)
@Query("DELETE FROM scores")
suspend fun deleteAllScores()
}

View file

@ -0,0 +1,21 @@
package com.accidentalproductions.tetristats.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.accidentalproductions.tetristats.TetriStatsApplication
import com.accidentalproductions.tetristats.util.Converters
@Database(entities = [Score::class], version = 1)
@TypeConverters(Converters::class)
abstract class ScoreDatabase : RoomDatabase() {
abstract fun scoreDao(): ScoreDao
companion object {
fun getDatabase(context: Context): ScoreDatabase {
return (context.applicationContext as TetriStatsApplication).database
}
}
}

View file

@ -0,0 +1,42 @@
package com.accidentalproductions.tetristats.ui.dashboard
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.accidentalproductions.tetristats.databinding.FragmentDashboardBinding
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val dashboardViewModel =
ViewModelProvider(this).get(DashboardViewModel::class.java)
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textDashboard
dashboardViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,13 @@
package com.accidentalproductions.tetristats.ui.dashboard
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class DashboardViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is dashboard Fragment"
}
val text: LiveData<String> = _text
}

View file

@ -0,0 +1,138 @@
package com.accidentalproductions.tetristats.ui.entry
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.databinding.FragmentEntryBinding
class EntryFragment : Fragment() {
private var _binding: FragmentEntryBinding? = null
private val binding get() = _binding!!
private val viewModel: EntryViewModel by viewModels { EntryViewModelFactory(requireActivity().application) }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentEntryBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupGameVersionDropdown()
setupScoreConverter()
setupSubmitButton()
}
private fun setupGameVersionDropdown() {
val games = listOf(
"NES Tetris",
"Game Boy Tetris",
"Tetris DX",
"Tetris DS",
"Tetris Effect",
"Rosy Retrospection DX",
"Apotris"
)
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
binding.autoCompleteGameVersion.setAdapter(adapter)
}
private fun setupScoreConverter() {
// Setup "From Game" dropdown with games that have scores
viewModel.gamesWithScores.observe(viewLifecycleOwner) { games ->
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
binding.autoCompleteFromGame.setAdapter(adapter)
}
// Update score selection when game is selected
binding.autoCompleteFromGame.setOnItemClickListener { _, _, _, _ ->
val selectedGame = binding.autoCompleteFromGame.text.toString()
viewModel.setSelectedFromGame(selectedGame)
updateScoreDropdown(selectedGame)
}
// Setup "To Game" dropdown
val allGames = listOf(
"NES Tetris",
"Game Boy Tetris",
"Tetris DX",
"Tetris DS",
"Tetris Effect",
"Rosy Retrospection DX",
"Apotris"
)
val toGameAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, allGames)
binding.autoCompleteToGame.setAdapter(toGameAdapter)
// Handle score conversion
binding.buttonConvert.setOnClickListener {
viewModel.convertScore()
}
// Observe converted score
viewModel.convertedScore.observe(viewLifecycleOwner) { score ->
binding.cardConvertedScore.visibility = View.VISIBLE
binding.textViewConvertedScore.text = "%,d".format(score)
}
// Update selected games
binding.autoCompleteToGame.setOnItemClickListener { _, _, _, _ ->
viewModel.setSelectedToGame(binding.autoCompleteToGame.text.toString())
}
}
private fun updateScoreDropdown(gameVersion: String) {
viewModel.getScoresForGame(gameVersion).observe(viewLifecycleOwner) { scores ->
val scoreStrings = scores.map { "${it.scoreValue} (Level ${it.endLevel ?: "?"})"}
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, scoreStrings)
binding.spinnerScoreSelect.setAdapter(adapter)
binding.spinnerScoreSelect.setOnItemClickListener { _, _, position, _ ->
viewModel.setSelectedScore(scores[position])
}
}
}
private fun setupSubmitButton() {
binding.buttonSubmit.setOnClickListener {
val gameVersion = binding.autoCompleteGameVersion.text.toString()
val score = binding.editTextScore.text.toString().toIntOrNull()
val startLevel = binding.editTextStartLevel.text.toString().toIntOrNull()
val endLevel = binding.editTextEndLevel.text.toString().toIntOrNull()
val linesCleared = binding.editTextLinesCleared.text.toString().toIntOrNull()
if (gameVersion.isNotEmpty() && score != null) {
viewModel.insertScore(
gameVersion = gameVersion,
score = score,
startLevel = startLevel,
endLevel = endLevel,
linesCleared = linesCleared
)
clearInputs()
}
}
}
private fun clearInputs() {
binding.autoCompleteGameVersion.text?.clear()
binding.editTextScore.text?.clear()
binding.editTextStartLevel.text?.clear()
binding.editTextEndLevel.text?.clear()
binding.editTextLinesCleared.text?.clear()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,78 @@
package com.accidentalproductions.tetristats.ui.entry
import android.app.Application
import androidx.lifecycle.*
import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.data.ScoreDatabase
import com.accidentalproductions.tetristats.data.ScalingFactors
import kotlinx.coroutines.launch
class EntryViewModel(application: Application) : AndroidViewModel(application) {
private val database = ScoreDatabase.getDatabase(application)
private val scoreDao = database.scoreDao()
val gamesWithScores = scoreDao.getGamesWithScores()
private val _selectedFromGame = MutableLiveData<String>()
private val _selectedScore = MutableLiveData<Score>()
private val _selectedToGame = MutableLiveData<String>()
private val _convertedScore = MutableLiveData<Int>()
val convertedScore: LiveData<Int> = _convertedScore
fun getScoresForGame(gameVersion: String): LiveData<List<Score>> {
return scoreDao.getScoresForGame(gameVersion)
}
fun setSelectedFromGame(game: String) {
_selectedFromGame.value = game
}
fun setSelectedScore(score: Score) {
_selectedScore.value = score
}
fun setSelectedToGame(game: String) {
_selectedToGame.value = game
}
fun convertScore() {
val fromGame = _selectedFromGame.value
val score = _selectedScore.value
val toGame = _selectedToGame.value
if (fromGame != null && score != null && toGame != null) {
val convertedScore = ScalingFactors.convertScore(fromGame, toGame, score.scoreValue)
_convertedScore.value = convertedScore
}
}
fun insertScore(
gameVersion: String,
score: Int,
startLevel: Int?,
endLevel: Int?,
linesCleared: Int?
) {
viewModelScope.launch {
val newScore = Score(
gameVersion = gameVersion,
scoreValue = score,
startLevel = startLevel,
endLevel = endLevel,
linesCleared = linesCleared
)
scoreDao.insert(newScore)
}
}
}
class EntryViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(EntryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return EntryViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -0,0 +1,52 @@
package com.accidentalproductions.tetristats.ui.history
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.accidentalproductions.tetristats.databinding.FragmentHistoryBinding
class HistoryFragment : Fragment() {
private var _binding: FragmentHistoryBinding? = null
private val binding get() = _binding!!
private val viewModel: HistoryViewModel by viewModels { HistoryViewModelFactory(requireActivity().application) }
private lateinit var scoreAdapter: ScoreAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHistoryBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeScores()
}
private fun setupRecyclerView() {
scoreAdapter = ScoreAdapter()
binding.recyclerViewHistory.apply {
adapter = scoreAdapter
layoutManager = LinearLayoutManager(context)
}
}
private fun observeScores() {
viewModel.allScores.observe(viewLifecycleOwner) { scores ->
scoreAdapter.submitList(scores)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,24 @@
package com.accidentalproductions.tetristats.ui.history
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.accidentalproductions.tetristats.data.ScoreDatabase
class HistoryViewModel(application: Application) : AndroidViewModel(application) {
private val database = ScoreDatabase.getDatabase(application)
private val scoreDao = database.scoreDao()
val allScores = scoreDao.getAllScores()
}
class HistoryViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(HistoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return HistoryViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -0,0 +1,54 @@
package com.accidentalproductions.tetristats.ui.history
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.databinding.ItemScoreBinding
import java.text.SimpleDateFormat
import java.util.Locale
class ScoreAdapter : ListAdapter<Score, ScoreAdapter.ScoreViewHolder>(ScoreDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScoreViewHolder {
val binding = ItemScoreBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ScoreViewHolder(binding)
}
override fun onBindViewHolder(holder: ScoreViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ScoreViewHolder(private val binding: ItemScoreBinding) : RecyclerView.ViewHolder(binding.root) {
private val dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault())
fun bind(score: Score) {
binding.textViewScore.text = "${score.scoreValue}"
binding.textViewDate.text = score.dateRecorded?.let { dateFormat.format(it) } ?: "Unknown"
val levelInfo = when {
score.startLevel != null && score.endLevel != null ->
"Levels ${score.startLevel}${score.endLevel}"
score.endLevel != null ->
"End Level: ${score.endLevel}"
else ->
""
}
binding.textViewLevelInfo.text = levelInfo
binding.textViewLinesCleared.text = score.linesCleared?.let { "Lines: $it" } ?: ""
}
}
}
class ScoreDiffCallback : DiffUtil.ItemCallback<Score>() {
override fun areItemsTheSame(oldItem: Score, newItem: Score): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Score, newItem: Score): Boolean {
return oldItem == newItem
}
}

View file

@ -0,0 +1,42 @@
package com.accidentalproductions.tetristats.ui.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.accidentalproductions.tetristats.databinding.FragmentHomeBinding
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val homeViewModel =
ViewModelProvider(this).get(HomeViewModel::class.java)
_binding = FragmentHomeBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textHome
homeViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,13 @@
package com.accidentalproductions.tetristats.ui.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class HomeViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is home Fragment"
}
val text: LiveData<String> = _text
}

View file

@ -0,0 +1,42 @@
package com.accidentalproductions.tetristats.ui.notifications
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.accidentalproductions.tetristats.databinding.FragmentNotificationsBinding
class NotificationsFragment : Fragment() {
private var _binding: FragmentNotificationsBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val notificationsViewModel =
ViewModelProvider(this).get(NotificationsViewModel::class.java)
_binding = FragmentNotificationsBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textNotifications
notificationsViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,13 @@
package com.accidentalproductions.tetristats.ui.notifications
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class NotificationsViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is notifications Fragment"
}
val text: LiveData<String> = _text
}

View file

@ -0,0 +1,54 @@
package com.accidentalproductions.tetristats.ui.stats
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.databinding.ItemScoreBinding
import java.text.SimpleDateFormat
import java.util.Locale
class ScoreAdapter : ListAdapter<Score, ScoreAdapter.ScoreViewHolder>(ScoreDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScoreViewHolder {
val binding = ItemScoreBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ScoreViewHolder(binding)
}
override fun onBindViewHolder(holder: ScoreViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ScoreViewHolder(private val binding: ItemScoreBinding) : RecyclerView.ViewHolder(binding.root) {
private val dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.getDefault())
fun bind(score: Score) {
binding.textViewScore.text = "${score.scoreValue}"
binding.textViewDate.text = score.dateRecorded?.let { dateFormat.format(it) } ?: "Unknown"
val levelInfo = when {
score.startLevel != null && score.endLevel != null ->
"Levels ${score.startLevel}${score.endLevel}"
score.endLevel != null ->
"End Level: ${score.endLevel}"
else ->
""
}
binding.textViewLevelInfo.text = levelInfo
binding.textViewLinesCleared.text = score.linesCleared?.let { "Lines: $it" } ?: ""
}
}
}
class ScoreDiffCallback : DiffUtil.ItemCallback<Score>() {
override fun areItemsTheSame(oldItem: Score, newItem: Score): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Score, newItem: Score): Boolean {
return oldItem == newItem
}
}

View file

@ -0,0 +1,74 @@
package com.accidentalproductions.tetristats.ui.stats
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.accidentalproductions.tetristats.databinding.FragmentStatsBinding
class StatsFragment : Fragment() {
private var _binding: FragmentStatsBinding? = null
private val binding get() = _binding!!
private val viewModel: StatsViewModel by viewModels { StatsViewModelFactory(requireActivity().application) }
private lateinit var scoreAdapter: ScoreAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentStatsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupGameFilter()
observeStats()
}
private fun setupRecyclerView() {
scoreAdapter = ScoreAdapter()
binding.recyclerViewScores.apply {
adapter = scoreAdapter
layoutManager = LinearLayoutManager(context)
}
}
private fun setupGameFilter() {
viewModel.gamesWithScores.observe(viewLifecycleOwner) { games ->
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, games)
binding.autoCompleteGameFilter.setAdapter(adapter)
}
binding.autoCompleteGameFilter.setOnItemClickListener { _, _, _, _ ->
val selectedGame = binding.autoCompleteGameFilter.text.toString()
viewModel.setSelectedGame(selectedGame)
}
}
private fun observeStats() {
viewModel.filteredScores.observe(viewLifecycleOwner) { scores ->
scoreAdapter.submitList(scores)
}
viewModel.averageScore.observe(viewLifecycleOwner) { average ->
binding.textViewAverageScore.text = "%.0f".format(average)
}
viewModel.highScore.observe(viewLifecycleOwner) { highScore ->
binding.textViewHighScore.text = "%,d".format(highScore)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -0,0 +1,42 @@
package com.accidentalproductions.tetristats.ui.stats
import android.app.Application
import androidx.lifecycle.*
import com.accidentalproductions.tetristats.data.Score
import com.accidentalproductions.tetristats.data.ScoreDatabase
class StatsViewModel(application: Application) : AndroidViewModel(application) {
private val database = ScoreDatabase.getDatabase(application)
private val scoreDao = database.scoreDao()
val gamesWithScores = scoreDao.getGamesWithScores()
private val _selectedGame = MutableLiveData<String>()
val selectedGame: LiveData<String> = _selectedGame
val filteredScores: LiveData<List<Score>> = _selectedGame.switchMap { game ->
scoreDao.getScoresForGame(game)
}
val averageScore: LiveData<Double> = _selectedGame.switchMap { game ->
scoreDao.getAverageScore(game)
}
val highScore: LiveData<Int> = _selectedGame.switchMap { game ->
scoreDao.getHighScore(game)
}
fun setSelectedGame(game: String) {
_selectedGame.value = game
}
}
class StatsViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(StatsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return StatsViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -0,0 +1,16 @@
package com.accidentalproductions.tetristats.util
import androidx.room.TypeConverter
import java.util.Date
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M13.5,8H12v5l4.28,2.54 0.72,-1.21 -3.5,-2.08V8zM13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12.5,8v4.25l3.5,2.08 -0.72,1.21L11,13V8h1.5z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Main T shape -->
<path
android:fillColor="@color/tetris_turquoise"
android:pathData="M30,30h48v16h-16v32h-16v-32h-16z"/>
<!-- Border lines -->
<path
android:strokeColor="@color/tetris_navy"
android:strokeWidth="2"
android:fillColor="#00000000"
android:pathData="M30,30h48v16h-16v32h-16v-32h-16z"/>
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17L7,17v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z"/>
</vector>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<fragment
android:id="@+id/nav_host_fragment_activity_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.dashboard.DashboardFragment">
<TextView
android:id="@+id/text_dashboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,221 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:spacing="16dp">
<!-- Score Entry Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
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="New Score Entry"
android:textAppearance="?attr/textAppearanceHeadline6"
android:layout_marginBottom="16dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteGameVersion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Game Version"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Score"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextStartLevel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Start Level"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextEndLevel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="End Level"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextLinesCleared"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Lines Cleared"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Submit Score"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Score Converter Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
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="Score Converter"
android:textAppearance="?attr/textAppearanceHeadline6"
android:layout_marginBottom="16dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteFromGame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="From Game"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/spinnerScoreSelect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Select Score"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteToGame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="To Game"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonConvert"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Convert Score"
android:layout_marginBottom="16dp"/>
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardConvertedScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:cardBackgroundColor="@color/tetris_turquoise"
app:cardElevation="2dp">
<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="Converted Score"
android:textColor="@color/tetris_navy"
android:textAppearance="?attr/textAppearanceBody1"/>
<TextView
android:id="@+id/textViewConvertedScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:textColor="@color/tetris_navy"
android:textAppearance="?attr/textAppearanceHeadline4"
tools:text="1,000,000"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:padding="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewHistory"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.home.HomeFragment">
<TextView
android:id="@+id/text_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.notifications.NotificationsFragment">
<TextView
android:id="@+id/text_notifications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/background">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/autoCompleteGameFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Filter by Game"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewScores"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Game Version">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextGameVersion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonUpdateStats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Update Stats" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="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="Average Score"
android:textAppearance="?attr/textAppearanceSubtitle1" />
<TextView
android:id="@+id/textViewAverageScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceHeadline4"
tools:text="1000" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="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="High Score"
android:textAppearance="?attr/textAppearanceSubtitle1" />
<TextView
android:id="@+id/textViewHighScore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceHeadline4"
tools:text="2000" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/textViewScore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline5"
android:textColor="@color/tetris_navy"
android:textStyle="bold" />
<TextView
android:id="@+id/textViewDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceBody2" />
<TextView
android:id="@+id/textViewLevelInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceBody2" />
<TextView
android:id="@+id/textViewLinesCleared"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textAppearance="?attr/textAppearanceBody2" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_entry"
android:icon="@drawable/ic_add_24"
android:title="Enter Score" />
<item
android:id="@+id/navigation_history"
android:icon="@drawable/ic_history_24"
android:title="History" />
<item
android:id="@+id/navigation_stats"
android:icon="@drawable/ic_stats_24"
android:title="Stats" />
</menu>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_entry">
<fragment
android:id="@+id/navigation_entry"
android:name="com.accidentalproductions.tetristats.ui.entry.EntryFragment"
android:label="Enter Score"
tools:layout="@layout/fragment_entry" />
<fragment
android:id="@+id/navigation_history"
android:name="com.accidentalproductions.tetristats.ui.history.HistoryFragment"
android:label="Score History"
tools:layout="@layout/fragment_history" />
<fragment
android:id="@+id/navigation_stats"
android:name="com.accidentalproductions.tetristats.ui.stats.StatsFragment"
android:label="Statistics"
tools:layout="@layout/fragment_stats" />
</navigation>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TetriStats" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/tetris_turquoise</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<item name="android:colorBackground">@color/tetris_navy</item>
<item name="colorSurface">@color/tetris_navy</item>
<item name="colorError">@color/error</item>
<!-- Text colors -->
<item name="android:textColorPrimary">@color/tetris_turquoise</item>
<item name="android:textColorSecondary">@color/tetris_pink</item>
<!-- Widget colors -->
<item name="materialButtonStyle">@style/Widget.TetriStats.Button.Night</item>
<item name="textInputStyle">@style/Widget.TetriStats.TextInputLayout.Night</item>
</style>
<style name="Widget.TetriStats.Button.Night" parent="Widget.MaterialComponents.Button">
<item name="backgroundTint">@color/tetris_pink</item>
<item name="android:textColor">@color/tetris_navy</item>
</style>
<style name="Widget.TetriStats.TextInputLayout.Night" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="boxStrokeColor">@color/tetris_pink</item>
<item name="hintTextColor">@color/tetris_turquoise</item>
<item name="android:textColorHint">@color/tetris_turquoise</item>
</style>
</resources>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Custom theme colors based on the Tetris image -->
<color name="primary">#5CD6D6</color>
<color name="primary_dark">#48AAAA</color>
<color name="accent">#FF9B9B</color>
<color name="background">#FFD5D5</color>
<color name="surface">#FFFFFF</color>
<color name="error">#FFB00020</color>
<color name="tetris_pink">#FFD5D5</color>
<color name="tetris_turquoise">#5CD6D6</color>
<color name="tetris_navy">#2D3B55</color>
<!-- Required Android default colors -->
<color name="purple_200">#5CD6D6</color>
<color name="purple_500">#48AAAA</color>
<color name="purple_700">#2D3B55</color>
<color name="teal_200">#5CD6D6</color>
<color name="teal_700">#48AAAA</color>
<color name="black">#000000</color>
<color name="white">#FFFFFF</color>
</resources>

View file

@ -0,0 +1,5 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">@color/tetris_pink</color>
</resources>

View file

@ -0,0 +1,6 @@
<resources>
<string name="app_name">TetriStats</string>
<string name="title_home">Home</string>
<string name="title_dashboard">Dashboard</string>
<string name="title_notifications">Notifications</string>
</resources>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.TetriStats" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<item name="android:colorBackground">@color/background</item>
<item name="colorSurface">@color/surface</item>
<item name="colorError">@color/error</item>
<!-- Text colors -->
<item name="android:textColorPrimary">@color/tetris_navy</item>
<item name="android:textColorSecondary">@color/tetris_navy</item>
<!-- Widget colors -->
<item name="materialButtonStyle">@style/Widget.TetriStats.Button</item>
<item name="textInputStyle">@style/Widget.TetriStats.TextInputLayout</item>
<!-- Splash screen -->
<item name="android:windowSplashScreenBackground">@color/tetris_pink</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/tetris_pink</item>
</style>
<style name="Widget.TetriStats.Button" parent="Widget.MaterialComponents.Button">
<item name="backgroundTint">@color/tetris_turquoise</item>
<item name="android:textColor">@color/tetris_navy</item>
</style>
<style name="Widget.TetriStats.TextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="boxStrokeColor">@color/tetris_turquoise</item>
<item name="hintTextColor">@color/tetris_navy</item>
</style>
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View file

@ -0,0 +1,17 @@
package com.accidentalproductions.tetristats
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}