commit ca8b4fd77b90fa959c714ea838327dc3a6a6fefa Author: cmclark00 Date: Thu Mar 20 01:14:21 2025 -0400 Initial commit: TetriStats Application with score tracking, conversion, and statistics for multiple Tetris games diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdeebc6 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..45a0477 --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..84740b8 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,8 @@ +/build +/captures +.externalNativeBuild +.cxx +local.properties + +# Keep Room schema files +!schemas/ \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..cd58820 --- /dev/null +++ b/app/build.gradle.kts @@ -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") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/schemas/com.accidentalproductions.tetristats.data.ScoreDatabase/1.json b/app/schemas/com.accidentalproductions.tetristats.data.ScoreDatabase/1.json new file mode 100644 index 0000000..6d10b5c --- /dev/null +++ b/app/schemas/com.accidentalproductions.tetristats.data.ScoreDatabase/1.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/accidentalproductions/tetristats/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/accidentalproductions/tetristats/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..e6f05d3 --- /dev/null +++ b/app/src/androidTest/java/com/accidentalproductions/tetristats/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a4eadbe --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/MainActivity.kt b/app/src/main/java/com/accidentalproductions/tetristats/MainActivity.kt new file mode 100644 index 0000000..140c5ff --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/MainActivity.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/TetriStatsApplication.kt b/app/src/main/java/com/accidentalproductions/tetristats/TetriStatsApplication.kt new file mode 100644 index 0000000..33da997 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/TetriStatsApplication.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/data/ScalingFactors.kt b/app/src/main/java/com/accidentalproductions/tetristats/data/ScalingFactors.kt new file mode 100644 index 0000000..93030f2 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/data/ScalingFactors.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/data/Score.kt b/app/src/main/java/com/accidentalproductions/tetristats/data/Score.kt new file mode 100644 index 0000000..f3ca9d8 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/data/Score.kt @@ -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() +) diff --git a/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt b/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt new file mode 100644 index 0000000..de27901 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt @@ -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> + + @Query("SELECT * FROM scores WHERE gameVersion = :gameVersion") + fun getScoresForGame(gameVersion: String): LiveData> + + @Query("SELECT DISTINCT gameVersion FROM scores") + fun getGamesWithScores(): LiveData> + + @Query("SELECT AVG(scoreValue) FROM scores WHERE gameVersion = :gameVersion") + fun getAverageScore(gameVersion: String): LiveData + + @Query("SELECT MAX(scoreValue) FROM scores WHERE gameVersion = :gameVersion") + fun getHighScore(gameVersion: String): LiveData + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(score: Score) + + @Delete + suspend fun delete(score: Score) + + @Query("DELETE FROM scores") + suspend fun deleteAllScores() +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDatabase.kt b/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDatabase.kt new file mode 100644 index 0000000..bb17a9c --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDatabase.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/dashboard/DashboardFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/dashboard/DashboardFragment.kt new file mode 100644 index 0000000..1f1c050 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/dashboard/DashboardFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/dashboard/DashboardViewModel.kt new file mode 100644 index 0000000..42fb5c0 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/dashboard/DashboardViewModel.kt @@ -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().apply { + value = "This is dashboard Fragment" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryFragment.kt new file mode 100644 index 0000000..f39d338 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryViewModel.kt new file mode 100644 index 0000000..2d1bcae --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/entry/EntryViewModel.kt @@ -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() + private val _selectedScore = MutableLiveData() + private val _selectedToGame = MutableLiveData() + private val _convertedScore = MutableLiveData() + + val convertedScore: LiveData = _convertedScore + + fun getScoresForGame(gameVersion: String): LiveData> { + 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 create(modelClass: Class): T { + if (modelClass.isAssignableFrom(EntryViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return EntryViewModel(application) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/history/HistoryFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/history/HistoryFragment.kt new file mode 100644 index 0000000..ce8a94b --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/history/HistoryFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/history/HistoryViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/history/HistoryViewModel.kt new file mode 100644 index 0000000..195c528 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/history/HistoryViewModel.kt @@ -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 create(modelClass: Class): T { + if (modelClass.isAssignableFrom(HistoryViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return HistoryViewModel(application) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/history/ScoreAdapter.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/history/ScoreAdapter.kt new file mode 100644 index 0000000..4b505fe --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/history/ScoreAdapter.kt @@ -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(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() { + override fun areItemsTheSame(oldItem: Score, newItem: Score): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Score, newItem: Score): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeFragment.kt new file mode 100644 index 0000000..34a7699 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeViewModel.kt new file mode 100644 index 0000000..dfd9e70 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/home/HomeViewModel.kt @@ -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().apply { + value = "This is home Fragment" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/notifications/NotificationsFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/notifications/NotificationsFragment.kt new file mode 100644 index 0000000..c29f178 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/notifications/NotificationsFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/notifications/NotificationsViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/notifications/NotificationsViewModel.kt new file mode 100644 index 0000000..8849463 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/notifications/NotificationsViewModel.kt @@ -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().apply { + value = "This is notifications Fragment" + } + val text: LiveData = _text +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/ScoreAdapter.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/ScoreAdapter.kt new file mode 100644 index 0000000..698ca3e --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/ScoreAdapter.kt @@ -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(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() { + override fun areItemsTheSame(oldItem: Score, newItem: Score): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: Score, newItem: Score): Boolean { + return oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt new file mode 100644 index 0000000..96c5ccb --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt new file mode 100644 index 0000000..dbd3bd7 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt @@ -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() + val selectedGame: LiveData = _selectedGame + + val filteredScores: LiveData> = _selectedGame.switchMap { game -> + scoreDao.getScoresForGame(game) + } + + val averageScore: LiveData = _selectedGame.switchMap { game -> + scoreDao.getAverageScore(game) + } + + val highScore: LiveData = _selectedGame.switchMap { game -> + scoreDao.getHighScore(game) + } + + fun setSelectedGame(game: String) { + _selectedGame.value = game + } +} + +class StatsViewModelFactory(private val application: Application) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(StatsViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return StatsViewModel(application) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/accidentalproductions/tetristats/util/Converters.kt b/app/src/main/java/com/accidentalproductions/tetristats/util/Converters.kt new file mode 100644 index 0000000..1ae7555 --- /dev/null +++ b/app/src/main/java/com/accidentalproductions/tetristats/util/Converters.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_24.xml b/app/src/main/res/drawable/ic_add_24.xml new file mode 100644 index 0000000..02ead86 --- /dev/null +++ b/app/src/main/res/drawable/ic_add_24.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml new file mode 100644 index 0000000..46fc8de --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_history_24.xml b/app/src/main/res/drawable/ic_history_24.xml new file mode 100644 index 0000000..2955747 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_24.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100644 index 0000000..f8bb0b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..72603f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 0000000..78b75c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_stats_24.xml b/app/src/main/res/drawable/ic_stats_24.xml new file mode 100644 index 0000000..c523f42 --- /dev/null +++ b/app/src/main/res/drawable/ic_stats_24.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..06ea6ca --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml new file mode 100644 index 0000000..166ab0e --- /dev/null +++ b/app/src/main/res/layout/fragment_dashboard.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_entry.xml b/app/src/main/res/layout/fragment_entry.xml new file mode 100644 index 0000000..f98930d --- /dev/null +++ b/app/src/main/res/layout/fragment_entry.xml @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_history.xml b/app/src/main/res/layout/fragment_history.xml new file mode 100644 index 0000000..2b4d422 --- /dev/null +++ b/app/src/main/res/layout/fragment_history.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..f3d9b08 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_notifications.xml b/app/src/main/res/layout/fragment_notifications.xml new file mode 100644 index 0000000..d417935 --- /dev/null +++ b/app/src/main/res/layout/fragment_notifications.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_stats.xml b/app/src/main/res/layout/fragment_stats.xml new file mode 100644 index 0000000..547976b --- /dev/null +++ b/app/src/main/res/layout/fragment_stats.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_score.xml b/app/src/main/res/layout/item_score.xml new file mode 100644 index 0000000..e44db93 --- /dev/null +++ b/app/src/main/res/layout/item_score.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml new file mode 100644 index 0000000..0498b82 --- /dev/null +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..bb831bd --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..bb831bd --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml new file mode 100644 index 0000000..d87901b --- /dev/null +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..e598bcc --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,31 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..43ce940 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + #5CD6D6 + #48AAAA + #FF9B9B + #FFD5D5 + #FFFFFF + #FFB00020 + #FFD5D5 + #5CD6D6 + #2D3B55 + + + #5CD6D6 + #48AAAA + #2D3B55 + #5CD6D6 + #48AAAA + #000000 + #FFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..e00c2dd --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..b70557c --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + @color/tetris_pink + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c592f8a --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + TetriStats + Home + Dashboard + Notifications + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..4985408 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,35 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..4df9255 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/accidentalproductions/tetristats/ExampleUnitTest.kt b/app/src/test/java/com/accidentalproductions/tetristats/ExampleUnitTest.kt new file mode 100644 index 0000000..12eae76 --- /dev/null +++ b/app/src/test/java/com/accidentalproductions/tetristats/ExampleUnitTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..0a7f377 --- /dev/null +++ b/build.gradle.kts @@ -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 +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..20e2a01 --- /dev/null +++ b/gradle.properties @@ -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 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d3d3132 --- /dev/null +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..af24903 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..acba73b --- /dev/null +++ b/settings.gradle.kts @@ -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")