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

55
.gitignore vendored Normal file
View file

@ -0,0 +1,55 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
.DS_Store
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
output-metadata.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
# Exceptions for Room schema files
!app/schemas/

48
README.md Normal file
View file

@ -0,0 +1,48 @@
# TetriStats
A modern Android application for tracking and comparing your Tetris scores across different game versions.
## Features
- **Score Tracking**: Log scores from different Tetris versions with details like start/end level and lines cleared
- **Score Conversion**: Convert scores between different Tetris games using accurate scaling factors
- **Statistics**: View your performance history and statistics
- **Game-Specific Adjustments**: Accounts for differences in scoring systems between games
## Supported Games
- NES Tetris
- Game Boy Tetris
- Tetris DX
- Tetris DS
- Tetris Effect
- Rosy Retrospection DX
- Apotris
## Technical Details
- Built with Kotlin
- Uses Android Architecture Components (ViewModel, LiveData, Room)
- Material Design UI components
- MVVM architecture
## Screenshots
[Screenshots coming soon]
## Installation
1. Download the latest APK from the Releases section
2. Enable installation from unknown sources in your device settings
3. Install the APK
Or build from source:
```
git clone https://github.com/yourusername/TetriStats.git
cd TetriStats
./gradlew assembleDebug
```
## License
MIT License

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

6
build.gradle.kts Normal file
View file

@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.ksp) apply false
}

23
gradle.properties Normal file
View file

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

39
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,39 @@
[versions]
agp = "8.9.0"
kotlin = "1.9.22"
ksp = "1.9.22-1.0.17"
coreKtx = "1.12.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
appcompat = "1.6.1"
material = "1.11.0"
constraintlayout = "2.1.4"
lifecycleLivedataKtx = "2.7.0"
lifecycleViewmodelKtx = "2.7.0"
navigationFragmentKtx = "2.7.7"
navigationUiKtx = "2.7.7"
room = "2.6.1"
mpchart = "v3.1.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
mpchart = { group = "com.github.PhilJay", name = "MPAndroidChart", version.ref = "mpchart" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Mon Mar 17 22:08:41 EDT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View file

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View file

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

24
settings.gradle.kts Normal file
View file

@ -0,0 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
rootProject.name = "TetriStats"
include(":app")