summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorA404M <ahmadmahmoudiprogrammer@gmail.com>2025-06-26 14:11:59 +0330
committerA404M <ahmadmahmoudiprogrammer@gmail.com>2025-06-26 14:11:59 +0330
commitb6ecd5026e5e64b5e8fb67f84d6c89ec6a24db31 (patch)
tree0c5cf7ac65ed1fcc4bd02ade34cc4607e88c85ed
inital commitv0.1.0
-rw-r--r--.gitignore15
-rw-r--r--.idea/.gitignore3
-rw-r--r--.idea/AndroidProjectSystem.xml6
-rw-r--r--.idea/compiler.xml6
-rw-r--r--.idea/deploymentTargetSelector.xml24
-rw-r--r--.idea/gradle.xml19
-rw-r--r--.idea/inspectionProfiles/Project_Default.xml61
-rw-r--r--.idea/kotlinc.xml6
-rw-r--r--.idea/migrations.xml10
-rw-r--r--.idea/misc.xml9
-rw-r--r--.idea/runConfigurations.xml17
-rw-r--r--.idea/statistic.xml6
-rw-r--r--.idea/vcs.xml6
-rw-r--r--app/.gitignore1
-rw-r--r--app/build.gradle.kts74
-rw-r--r--app/proguard-rules.pro21
-rw-r--r--app/release/app-release.apkbin0 -> 1214911 bytes
-rw-r--r--app/release/baselineProfiles/0/app-release.dmbin0 -> 4727 bytes
-rw-r--r--app/release/baselineProfiles/1/app-release.dmbin0 -> 4684 bytes
-rw-r--r--app/release/output-metadata.json37
-rw-r--r--app/src/androidTest/java/com/a404m/mine_game/ExampleInstrumentedTest.kt27
-rw-r--r--app/src/main/AndroidManifest.xml29
-rw-r--r--app/src/main/ic_launcher-playstore.pngbin0 -> 13958 bytes
-rw-r--r--app/src/main/java/com/a404m/mine_game/ContextHelper.kt9
-rw-r--r--app/src/main/java/com/a404m/mine_game/MainActivity.kt38
-rw-r--r--app/src/main/java/com/a404m/mine_game/core/Constants.kt4
-rw-r--r--app/src/main/java/com/a404m/mine_game/model/Action.kt22
-rw-r--r--app/src/main/java/com/a404m/mine_game/model/GameSettings.kt49
-rw-r--r--app/src/main/java/com/a404m/mine_game/model/GameState.kt236
-rw-r--r--app/src/main/java/com/a404m/mine_game/model/JsonSerializable.kt7
-rw-r--r--app/src/main/java/com/a404m/mine_game/storage/StorageBase.kt127
-rw-r--r--app/src/main/java/com/a404m/mine_game/storage/StorageGame.kt50
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/page/Controls.kt78
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/page/Game.kt442
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/page/Home.kt286
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/page/Route.kt166
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/page/Settings.kt259
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/page/Splash.kt38
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/theme/Color.kt11
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/theme/Font.kt27
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/theme/Theme.kt82
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/theme/Type.kt34
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/utils/Action.kt62
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/utils/AppBar.kt54
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/utils/Container.kt160
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/utils/Dialog.kt47
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/utils/Dropdown.kt86
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/utils/Toast.kt15
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/utils/modifier/Extension.kt323
-rw-r--r--app/src/main/java/com/a404m/mine_game/ui/utils/modifier/model/FreeScrollState.kt141
-rw-r--r--app/src/main/java/com/a404m/mine_game/utils/Application.kt14
-rw-r--r--app/src/main/java/com/a404m/mine_game/utils/Extensions.kt85
-rw-r--r--app/src/main/java/com/a404m/mine_game/utils/PersianDate.kt539
-rw-r--r--app/src/main/res/drawable/about_us.xml5
-rw-r--r--app/src/main/res/drawable/add.xml5
-rw-r--r--app/src/main/res/drawable/arrow_back.xml5
-rw-r--r--app/src/main/res/drawable/arrow_drop_down.xml5
-rw-r--r--app/src/main/res/drawable/arrow_drop_up.xml5
-rw-r--r--app/src/main/res/drawable/controls.xml5
-rw-r--r--app/src/main/res/drawable/delete.xml5
-rw-r--r--app/src/main/res/drawable/donate.xml5
-rw-r--r--app/src/main/res/drawable/flag.xml5
-rw-r--r--app/src/main/res/drawable/hint.xml5
-rw-r--r--app/src/main/res/drawable/history.xml5
-rw-r--r--app/src/main/res/drawable/ic_launcher_foreground.xml21
-rw-r--r--app/src/main/res/drawable/language.xml5
-rw-r--r--app/src/main/res/drawable/mine.xml16
-rw-r--r--app/src/main/res/drawable/open.xml5
-rw-r--r--app/src/main/res/drawable/play.xml5
-rw-r--r--app/src/main/res/drawable/save.xml5
-rw-r--r--app/src/main/res/drawable/settings.xml5
-rw-r--r--app/src/main/res/drawable/statistics.xml9
-rw-r--r--app/src/main/res/drawable/theme.xml5
-rw-r--r--app/src/main/res/drawable/time.xml5
-rw-r--r--app/src/main/res/drawable/tutorial.xml5
-rw-r--r--app/src/main/res/font/iranyekan_bold.ttfbin0 -> 59528 bytes
-rw-r--r--app/src/main/res/font/iranyekan_light.ttfbin0 -> 61488 bytes
-rw-r--r--app/src/main/res/font/iranyekan_medium.ttfbin0 -> 60404 bytes
-rw-r--r--app/src/main/res/font/iranyekan_regular.ttfbin0 -> 60268 bytes
-rw-r--r--app/src/main/res/font/vazirmatn.ttfbin0 -> 241328 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_black.ttfbin0 -> 123376 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_bold.ttfbin0 -> 123036 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_extrabold.ttfbin0 -> 123324 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_extralight.ttfbin0 -> 123292 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_light.ttfbin0 -> 122920 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_medium.ttfbin0 -> 122656 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_regular.ttfbin0 -> 122752 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_semibold.ttfbin0 -> 122920 bytes
-rw-r--r--app/src/main/res/font/vazirmatn_thin.ttfbin0 -> 124232 bytes
-rw-r--r--app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml5
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher.webpbin0 -> 864 bytes
-rw-r--r--app/src/main/res/mipmap-hdpi/ic_launcher_round.webpbin0 -> 2058 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher.webpbin0 -> 590 bytes
-rw-r--r--app/src/main/res/mipmap-mdpi/ic_launcher_round.webpbin0 -> 1290 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher.webpbin0 -> 1148 bytes
-rw-r--r--app/src/main/res/mipmap-xhdpi/ic_launcher_round.webpbin0 -> 2890 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher.webpbin0 -> 1614 bytes
-rw-r--r--app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webpbin0 -> 4464 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher.webpbin0 -> 2226 bytes
-rw-r--r--app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webpbin0 -> 6498 bytes
-rw-r--r--app/src/main/res/values/colors.xml10
-rw-r--r--app/src/main/res/values/ic_launcher_background.xml4
-rw-r--r--app/src/main/res/values/strings.xml3
-rw-r--r--app/src/main/res/values/themes.xml5
-rw-r--r--app/src/main/res/xml/backup_rules.xml13
-rw-r--r--app/src/main/res/xml/data_extraction_rules.xml19
-rw-r--r--app/src/test/java/com/a404m/mine_game/ExampleUnitTest.kt20
-rw-r--r--build.gradle.kts6
-rw-r--r--gradle.properties23
-rw-r--r--gradle/libs.versions.toml40
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 59203 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xgradlew185
-rw-r--r--gradlew.bat89
-rw-r--r--settings.gradle.kts24
116 files changed, 4466 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml
new file mode 100644
index 0000000..4a53bee
--- /dev/null
+++ b/.idea/AndroidProjectSystem.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="AndroidProjectSystem">
+ <option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b86273d
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <bytecodeTargetLevel target="21" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..04cb880
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="deploymentTargetSelector">
+ <selectionStates>
+ <SelectionState runConfigName="app">
+ <option name="selectionMode" value="DROPDOWN" />
+ <DropdownSelection timestamp="2025-06-25T19:28:28.657577745Z">
+ <Target type="DEFAULT_BOOT">
+ <handle>
+ <DeviceId pluginId="PhysicalDevice" identifier="serial=ww7la69hnrhym7yh" />
+ </handle>
+ </Target>
+ </DropdownSelection>
+ <DialogSelection />
+ </SelectionState>
+ <SelectionState runConfigName="WorkersPreview">
+ <option name="selectionMode" value="DROPDOWN" />
+ </SelectionState>
+ <SelectionState runConfigName="WorkerSettingsPreview">
+ <option name="selectionMode" value="DROPDOWN" />
+ </SelectionState>
+ </selectionStates>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..639c779
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="GradleMigrationSettings" migrationVersion="1" />
+ <component name="GradleSettings">
+ <option name="linkedExternalProjectsSettings">
+ <GradleProjectSettings>
+ <option name="testRunner" value="CHOOSE_PER_TEST" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$" />
+ <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
+ <option name="modules">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ <option value="$PROJECT_DIR$/app" />
+ </set>
+ </option>
+ </GradleProjectSettings>
+ </option>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..7061a0d
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,61 @@
+<component name="InspectionProjectProfileManager">
+ <profile version="1.0">
+ <option name="myName" value="Project Default" />
+ <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+ <option name="composableFile" value="true" />
+ <option name="previewFile" value="true" />
+ </inspection_tool>
+ </profile>
+</component> \ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..6d0ee1c
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="KotlinJpsPluginSettings">
+ <option name="version" value="2.0.0" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectMigrations">
+ <option name="MigrateToGradleLocalJavaHome">
+ <set>
+ <option value="$PROJECT_DIR$" />
+ </set>
+ </option>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..b2c751a
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+<project version="4">
+ <component name="ExternalStorageConfigurationManager" enabled="true" />
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/build/classes" />
+ </component>
+ <component name="ProjectType">
+ <option name="id" value="Android" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..16660f1
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="RunConfigurationProducerService">
+ <option name="ignoredProducers">
+ <set>
+ <option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
+ <option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
+ <option value="com.intellij.execution.junit.PatternConfigurationProducer" />
+ <option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
+ <option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
+ <option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
+ <option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
+ <option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
+ </set>
+ </option>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/statistic.xml b/.idea/statistic.xml
new file mode 100644
index 0000000..5b8a46d
--- /dev/null
+++ b/.idea/statistic.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="Statistic">
+ <option name="fileTypesIncluded" value="kt" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..7511bf2
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,74 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "com.a404m.mine_game"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.a404m.mine_game"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "0.1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ isShrinkResources = true
+ isProfileable = false
+ isDebuggable = false
+ isJniDebuggable = false
+ isPseudoLocalesEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.material3)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+
+ // navigation
+ implementation(libs.androidx.navigation.compose)
+
+ // AsyncImage
+ // implementation(libs.coil.compose)
+
+ // http
+ // implementation(libs.okhttp)
+} \ 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/release/app-release.apk b/app/release/app-release.apk
new file mode 100644
index 0000000..ab02a01
--- /dev/null
+++ b/app/release/app-release.apk
Binary files differ
diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm
new file mode 100644
index 0000000..c96ca9d
--- /dev/null
+++ b/app/release/baselineProfiles/0/app-release.dm
Binary files differ
diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm
new file mode 100644
index 0000000..38b3a4e
--- /dev/null
+++ b/app/release/baselineProfiles/1/app-release.dm
Binary files differ
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
new file mode 100644
index 0000000..b693519
--- /dev/null
+++ b/app/release/output-metadata.json
@@ -0,0 +1,37 @@
+{
+ "version": 3,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "com.a404m.mine_game",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "attributes": [],
+ "versionCode": 1,
+ "versionName": "0.1.0",
+ "outputFile": "app-release.apk"
+ }
+ ],
+ "elementType": "File",
+ "baselineProfiles": [
+ {
+ "minApi": 28,
+ "maxApi": 30,
+ "baselineProfiles": [
+ "baselineProfiles/1/app-release.dm"
+ ]
+ },
+ {
+ "minApi": 31,
+ "maxApi": 2147483647,
+ "baselineProfiles": [
+ "baselineProfiles/0/app-release.dm"
+ ]
+ }
+ ],
+ "minSdkVersionForDexing": 24
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/com/a404m/mine_game/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/a404m/mine_game/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..b72d64b
--- /dev/null
+++ b/app/src/androidTest/java/com/a404m/mine_game/ExampleInstrumentedTest.kt
@@ -0,0 +1,27 @@
+package com.a404m.mine_game
+
+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.a404m.mine_game",
+ 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..7a0aa4c
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?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: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.Chat"
+ android:largeHeap="true"
+ tools:targetApi="31">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true"
+ android:theme="@style/Theme.Chat"
+ android:windowSoftInputMode="adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..ed8c430
--- /dev/null
+++ b/app/src/main/ic_launcher-playstore.png
Binary files differ
diff --git a/app/src/main/java/com/a404m/mine_game/ContextHelper.kt b/app/src/main/java/com/a404m/mine_game/ContextHelper.kt
new file mode 100644
index 0000000..4203878
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ContextHelper.kt
@@ -0,0 +1,9 @@
+package com.a404m.mine_game
+
+import android.annotation.SuppressLint
+import androidx.activity.ComponentActivity
+
+@SuppressLint("StaticFieldLeak")
+object ContextHelper {
+ lateinit var context: ComponentActivity
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/MainActivity.kt b/app/src/main/java/com/a404m/mine_game/MainActivity.kt
new file mode 100644
index 0000000..7a7a275
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/MainActivity.kt
@@ -0,0 +1,38 @@
+package com.a404m.mine_game
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import com.a404m.mine_game.core.TAG
+import com.a404m.mine_game.model.GameState
+import com.a404m.mine_game.storage.StorageGame
+import com.a404m.mine_game.ui.page.Route
+import com.a404m.mine_game.ui.theme.MineGameTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ ContextHelper.context = this
+
+ setContent {
+ MineGameTheme {
+ Route()
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ Log.d(
+ TAG,
+ "onPause: called"
+ )
+ val gameState = GameState.current
+ if(gameState != null) {
+ StorageGame.save(gameState)
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/core/Constants.kt b/app/src/main/java/com/a404m/mine_game/core/Constants.kt
new file mode 100644
index 0000000..91859a6
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/core/Constants.kt
@@ -0,0 +1,4 @@
+package com.a404m.mine_game.core
+
+
+const val TAG = "A404M"
diff --git a/app/src/main/java/com/a404m/mine_game/model/Action.kt b/app/src/main/java/com/a404m/mine_game/model/Action.kt
new file mode 100644
index 0000000..08d7ecf
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/model/Action.kt
@@ -0,0 +1,22 @@
+package com.a404m.mine_game.model
+
+import androidx.annotation.DrawableRes
+import com.a404m.mine_game.R
+
+enum class Action(
+ @DrawableRes val icon: Int,
+) {
+ FLAG(
+ icon = R.drawable.flag,
+ ),
+ OPEN(
+ icon = R.drawable.open,
+ ),
+ ;
+
+ companion object{
+ var current = OPEN
+
+ fun from(value:Int) = entries.find { it.ordinal == value }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/model/GameSettings.kt b/app/src/main/java/com/a404m/mine_game/model/GameSettings.kt
new file mode 100644
index 0000000..2889ce8
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/model/GameSettings.kt
@@ -0,0 +1,49 @@
+package com.a404m.mine_game.model
+
+import com.a404m.mine_game.storage.StorageGame
+import com.a404m.mine_game.utils.getIntOrNull
+import org.json.JSONObject
+
+data class GameSettings(
+ val width: Int,
+ val height: Int,
+ val mines: Int,
+ val seed: Int?,
+) : JsonSerializable {
+
+ constructor(json: JSONObject) : this(
+ width = json.getInt("width"),
+ height = json.getInt("height"),
+ mines = json.getInt("mines"),
+ seed = json.getIntOrNull("seed"),
+ )
+
+ override fun toJson(): JSONObject = JSONObject()
+ .put(
+ "width",
+ width,
+ )
+ .put(
+ "height",
+ height,
+ )
+ .put(
+ "mines",
+ mines,
+ )
+ .put(
+ "seed",
+ seed,
+ )
+
+ companion object {
+ val default = GameSettings(
+ width = 9,
+ height = 9,
+ mines = 10,
+ seed = null,
+ )
+
+ var current = StorageGame.getGameSettings() ?: default
+ }
+}
diff --git a/app/src/main/java/com/a404m/mine_game/model/GameState.kt b/app/src/main/java/com/a404m/mine_game/model/GameState.kt
new file mode 100644
index 0000000..69d022a
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/model/GameState.kt
@@ -0,0 +1,236 @@
+package com.a404m.mine_game.model
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import com.a404m.mine_game.utils.PersianDate
+import com.a404m.mine_game.utils.map
+import com.a404m.mine_game.utils.toJson
+import org.json.JSONArray
+import org.json.JSONObject
+import kotlin.random.Random
+
+class GameState(
+ val startDate: PersianDate,
+ val matrix: ArrayList<ArrayList<Cell>>,
+ millis: Long = 0,
+) : JsonSerializable {
+ var millis by mutableLongStateOf(millis)
+
+ val seconds:Int
+ get() = ((millis/1000)%60).toInt()
+
+ val minutes:Int
+ get() = ((millis/1000)/60).toInt()
+
+ val remainingBombs: Int
+ get() = matrix.sumOf { it.count { cell -> cell.isBomb && !cell.isOpened } }
+
+ val flagCounts: Int
+ get() = matrix.sumOf { it.count { cell -> cell.isFlag } }
+
+ val bombCount: Int
+ get() = matrix.sumOf { it.count { cell -> cell.isBomb } }
+
+ constructor(
+ width: Int,
+ height: Int,
+ mines: Int,
+ seed: Int?,
+ ) : this(
+ startDate = PersianDate(),
+ matrix = arrayListOf<ArrayList<Cell>>().apply {
+ ensureCapacity(width)
+ var cellRemaining = width * height
+ if (cellRemaining < mines) {
+ throw IllegalStateException("Mines are more than cells $mines > $cellRemaining")
+ }
+ var bombsRemaining = mines
+
+ val random = if (seed == null) Random else Random(seed)
+
+ for (i in 0 until width) {
+ add(
+ arrayListOf<Cell>().apply {
+ ensureCapacity(height)
+ for (j in 0 until height) {
+ val isBomb = random.nextInt(cellRemaining) < bombsRemaining
+ add(
+ Cell(
+ isBomb = isBomb,
+ )
+ )
+ cellRemaining -= 1
+ if (isBomb) {
+ bombsRemaining -= 1
+ }
+ }
+ }
+ )
+ }
+
+ for (i in this.indices) {
+ val items = this[i]
+ for (j in items.indices) {
+ val cell = items[j]
+
+ var count = 0
+
+ for (i0 in -1..1) {
+ val index0 = i + i0
+ if (index0 !in this.indices) {
+ continue
+ }
+ val row = this[index0]
+ for (j0 in -1..1) {
+ val index1 = j + j0
+ if (index1 !in row.indices) {
+ continue
+ }
+ if (row[index1].isBomb) {
+ count++
+ }
+ }
+ }
+
+ cell.setCountAround(count)
+ }
+ }
+ }
+ )
+
+ constructor(json: JSONObject) : this(
+ startDate = PersianDate(json.getLong("start_date")),
+ matrix = ArrayList(json.getJSONArray("matrix").map {
+ ArrayList((it as JSONArray).map { cell ->
+ Cell(cell as JSONObject)
+ })
+ }),
+ millis = json.getLong("millis"),
+ )
+
+ override fun toJson(): JSONObject = JSONObject()
+ .put(
+ "start_date",
+ startDate.getTime(),
+ )
+ .put(
+ "matrix",
+ matrix.map {
+ it.map {
+ it.toJson()
+ }.toJson()
+ }.toJson()
+ )
+ .put(
+ "millis",
+ millis,
+ )
+
+ fun openNearbyCells(
+ i: Int,
+ j: Int,
+ ) {
+ if (i !in matrix.indices || j !in matrix[i].indices) {
+ return
+ }
+ val cell = matrix[i][j]
+
+ if (cell.isOpened) {
+ return
+ }
+ cell.isOpened = true
+
+ if (cell.countAround != 0) {
+ return
+ }
+
+ for (di in -1..1) {
+ for (dj in -1..1) {
+ if (di == 0 && dj == 0)
+ continue
+
+ openNearbyCells(
+ i + di,
+ j + dj
+ )
+ }
+ }
+ }
+
+ fun isLost(): Boolean {
+ return matrix.any { it.any { cell -> cell.isBomb && cell.isOpened } }
+ }
+
+ fun isWon(): Boolean {
+ return matrix.all { it.all { cell -> (cell.isBomb && cell.isFlag) || (!cell.isBomb && cell.isOpened) } }
+ }
+
+ class Cell(
+ flag: Int,
+ ) {
+ private var flagValue by mutableIntStateOf(flag)
+
+ constructor(
+ isBomb: Boolean,
+ ) : this(
+ flag = if (isBomb) BOMB_BIT else 0,
+ )
+
+ constructor(json: JSONObject) : this(
+ flag = json.getInt("flag"),
+ )
+
+ fun toJson(): JSONObject = JSONObject()
+ .put(
+ "flag",
+ flagValue,
+ )
+
+ val countAround: Int
+ get() = (flagValue and COUNT_BITS)
+
+ var isOpened: Boolean
+ get() = (flagValue and OPENED_BIT) != 0
+ set(value) {
+ flagValue = if (value) {
+ flagValue or OPENED_BIT
+ } else {
+ flagValue and OPENED_BIT.inv()
+ }
+ }
+
+ val isBomb: Boolean
+ get() = (flagValue and BOMB_BIT) != 0
+
+ var isFlag: Boolean
+ get() = (flagValue and FLAG_BIT) != 0
+ set(value) {
+ flagValue = if (value) {
+ flagValue or FLAG_BIT
+ } else {
+ flagValue and FLAG_BIT.inv()
+ }
+ }
+
+ fun setCountAround(count: Int) {
+ if (count !in 0..9) {
+ throw IllegalStateException("Bad count $count")
+ }
+ flagValue = (flagValue and COUNT_BITS.inv()) or count
+ }
+
+ companion object {
+ private const val COUNT_BITS = 0b1111
+ private const val OPENED_BIT = 0b10000
+ private const val BOMB_BIT = 0b100000
+ private const val FLAG_BIT = 0b1000000
+ }
+ }
+
+ companion object {
+ var current by mutableStateOf<GameState?>(null)
+ }
+}
diff --git a/app/src/main/java/com/a404m/mine_game/model/JsonSerializable.kt b/app/src/main/java/com/a404m/mine_game/model/JsonSerializable.kt
new file mode 100644
index 0000000..532f130
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/model/JsonSerializable.kt
@@ -0,0 +1,7 @@
+package com.a404m.mine_game.model
+
+import org.json.JSONObject
+
+interface JsonSerializable {
+ fun toJson():JSONObject
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/storage/StorageBase.kt b/app/src/main/java/com/a404m/mine_game/storage/StorageBase.kt
new file mode 100644
index 0000000..d3e0269
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/storage/StorageBase.kt
@@ -0,0 +1,127 @@
+package com.a404m.mine_game.storage
+
+import androidx.core.content.edit
+import com.a404m.mine_game.ContextHelper
+import com.a404m.mine_game.model.JsonSerializable
+import org.json.JSONObject
+
+open class StorageBase(
+ private val storageKey: String,
+) {
+ private fun getPrefs() = ContextHelper.context.getSharedPreferences(
+ storageKey,
+ 0,
+ )
+
+ protected fun save(
+ key: String,
+ value: String,
+ ) = getPrefs().edit {
+ putString(
+ key,
+ value,
+ )
+ }
+
+ protected fun save(
+ key: String,
+ value: Int,
+ ) = getPrefs().edit {
+ putInt(
+ key,
+ value,
+ )
+ }
+
+ protected fun save(
+ key: String,
+ value: Long,
+ ) = getPrefs().edit {
+ putLong(
+ key,
+ value,
+ )
+ }
+
+ protected fun save(
+ key: String,
+ value: Float,
+ ) = getPrefs().edit {
+ putFloat(
+ key,
+ value,
+ )
+ }
+
+ protected fun save(
+ key: String,
+ value: Boolean,
+ ) = getPrefs().edit {
+ putBoolean(
+ key,
+ value,
+ )
+ }
+
+ protected fun save(
+ key: String,
+ value: Set<String>,
+ ) = getPrefs().edit {
+ putStringSet(
+ key,
+ value,
+ )
+ }
+
+ protected fun save(
+ key: String,
+ value: JsonSerializable,
+ ) = getPrefs().edit {
+ putString(
+ key,
+ value.toJson().toString(),
+ )
+ }
+
+ protected fun getString(key: String) = getPrefs().getString(
+ key,
+ null,
+ )
+
+ protected fun getInt(key: String) = getPrefs().getInt(
+ key,
+ -1,
+ )
+
+ protected fun getLong(key: String) = getPrefs().getLong(
+ key,
+ -1,
+ )
+
+ protected fun getFloat(key: String) = getPrefs().getFloat(
+ key,
+ -1f,
+ )
+
+ protected fun getBoolean(key: String) = getPrefs().getBoolean(
+ key,
+ false,
+ )
+
+ protected fun getStringSet(key: String) = getPrefs().getStringSet(
+ key,
+ null,
+ )
+
+ protected fun getJson(key: String): JSONObject? {
+ val value = getPrefs().getString(
+ key,
+ null,
+ ) ?: return null
+ return JSONObject(value)
+ }
+
+ protected fun contains(key: String) = getPrefs().contains(key)
+
+ protected fun delete(key: String) = getPrefs().edit { remove(key) }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/storage/StorageGame.kt b/app/src/main/java/com/a404m/mine_game/storage/StorageGame.kt
new file mode 100644
index 0000000..402fe20
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/storage/StorageGame.kt
@@ -0,0 +1,50 @@
+package com.a404m.mine_game.storage
+
+import com.a404m.mine_game.model.Action
+import com.a404m.mine_game.model.GameSettings
+import com.a404m.mine_game.model.GameState
+
+object StorageGame : StorageBase("StorageSettings") {
+ private const val GAME_SETTINGS_KEY = "game_settings"
+ private const val LAST_GAME_STATE_KEY = "last_game"
+ private const val PRIMARY_ACTION_KEY = "primary_action"
+
+ fun save(value: GameSettings) = save(
+ GAME_SETTINGS_KEY,
+ value,
+ )
+
+ fun getGameSettings(): GameSettings? {
+ val json = getJson(GAME_SETTINGS_KEY) ?: return null
+
+ return GameSettings(json)
+ }
+
+ fun deleteGameSettings() = delete(GAME_SETTINGS_KEY)
+
+ fun save(value: GameState) = save(
+ LAST_GAME_STATE_KEY,
+ value,
+ )
+
+ fun getLastGame(): GameState? {
+ val json = getJson(LAST_GAME_STATE_KEY) ?: return null
+
+ return GameState(json)
+ }
+
+ fun deleteLastGame() = delete(LAST_GAME_STATE_KEY)
+
+ fun savePrimaryAction(value: Action) = save(
+ PRIMARY_ACTION_KEY,
+ value.ordinal,
+ )
+
+ fun getPrimaryAction(): Action? {
+ val value = getInt(PRIMARY_ACTION_KEY)
+
+ return Action.from(value)
+ }
+
+ fun deletePrimaryAction() = delete(PRIMARY_ACTION_KEY)
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/page/Controls.kt b/app/src/main/java/com/a404m/mine_game/ui/page/Controls.kt
new file mode 100644
index 0000000..02ea22a
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/page/Controls.kt
@@ -0,0 +1,78 @@
+package com.a404m.mine_game.ui.page
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.a404m.mine_game.model.Action
+import com.a404m.mine_game.storage.StorageGame
+import com.a404m.mine_game.ui.utils.ActionChooser
+import com.a404m.mine_game.ui.utils.CustomScaffold
+import com.a404m.mine_game.ui.utils.TitledColumn
+import com.a404m.mine_game.ui.utils.TopBar
+
+
+@Composable
+fun ControlsPage(
+ modifier: Modifier = Modifier,
+ onBack: () -> Unit,
+) {
+ var selectedAction by rememberSaveable { mutableStateOf(Action.current) }
+
+ DisposableEffect(Unit) {
+ onDispose {
+ Action.current = selectedAction
+ StorageGame.savePrimaryAction(selectedAction)
+ }
+ }
+
+ CustomScaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = { innerPadding ->
+ TopBar(
+ modifier = Modifier.padding(innerPadding),
+ title = "Controls",
+ onBack = onBack,
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(innerPadding)
+ .padding(10.dp),
+ ) {
+ TitledColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = MaterialTheme.shapes.medium,
+ ),
+ title = "Default Button",
+ ) {
+ ActionChooser(
+ modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 10.dp, bottom = 15.dp),
+ selected = selectedAction,
+ onSelect = {
+ selectedAction = it
+ },
+ )
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/page/Game.kt b/app/src/main/java/com/a404m/mine_game/ui/page/Game.kt
new file mode 100644
index 0000000..cf3b991
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/page/Game.kt
@@ -0,0 +1,442 @@
+package com.a404m.mine_game.ui.page
+
+import android.util.Log
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ElevatedButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import com.a404m.mine_game.R
+import com.a404m.mine_game.core.TAG
+import com.a404m.mine_game.model.Action
+import com.a404m.mine_game.model.GameState
+import com.a404m.mine_game.storage.StorageGame
+import com.a404m.mine_game.ui.utils.ActionChooser
+import com.a404m.mine_game.ui.utils.CustomScaffold
+import com.a404m.mine_game.ui.utils.modifier.freeScrollWithTransformGesture
+import com.a404m.mine_game.ui.utils.modifier.model.rememberFreeScrollState
+import com.a404m.mine_game.utils.PersianDate
+import kotlinx.coroutines.delay
+
+
+@Composable
+fun GamePage(
+ modifier: Modifier = Modifier,
+ onBack: () -> Unit,
+ onNewGame: () -> Unit,
+ gameState: GameState,
+) {
+ val time by remember {
+ derivedStateOf {
+ "${gameState.minutes}:${
+ gameState.seconds.toString().padStart(
+ 2,
+ '0'
+ )
+ }"
+ }
+ }
+ val minesRemaining by remember { derivedStateOf { gameState.remainingBombs - gameState.flagCounts } }
+ val isLost by remember {
+ derivedStateOf {
+ gameState.isLost()
+ }
+ }
+ val isWon by remember {
+ derivedStateOf {
+ gameState.isWon()
+ }
+ }
+
+ var action by rememberSaveable { mutableStateOf(Action.current) }
+
+ var zoomOverall by rememberSaveable { mutableFloatStateOf(1f) }
+
+ LaunchedEffect(Unit) {
+ val startTime = PersianDate()
+ val startingMillis = gameState.millis
+ while (!isWon && !isLost) {
+ delay(50)
+ gameState.millis = startingMillis + PersianDate().getTime() - startTime.getTime()
+ }
+ }
+
+ DisposableEffect(Unit) {
+ onDispose {
+ StorageGame.save(gameState)
+ }
+ }
+
+ CustomScaffold(
+ topBar = { innerPadding ->
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(
+ color = MaterialTheme.colorScheme.background.copy(alpha = 0.9f),
+ )
+ .padding(innerPadding)
+ .padding(5.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ IconButton(
+ modifier = Modifier.padding(end = 10.dp),
+ onClick = onBack,
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.arrow_back),
+ contentDescription = "Back",
+ )
+ }
+ Row(
+ modifier = Modifier.weight(1F),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ LabeledIcon(
+ modifier = Modifier.padding(end = 12.dp),
+ icon = R.drawable.time,
+ contentDescription = "Time",
+ text = time,
+ )
+ LabeledIcon(
+ icon = R.drawable.mine,
+ contentDescription = "Mines remaining",
+ text = minesRemaining.toString(),
+ )
+ }
+ IconButton(
+ modifier = Modifier.padding(end = 10.dp),
+ onClick = onBack,
+ ) {
+ Row(
+ modifier = Modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ modifier = Modifier,
+ text = "6",
+ style = MaterialTheme.typography.bodySmall,
+ )
+ Icon(
+ painter = painterResource(R.drawable.hint),
+ contentDescription = "Back",
+ )
+ }
+ }
+ }
+ }
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .freeScrollWithTransformGesture(
+ rememberFreeScrollState(),
+ onGesture = { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
+ zoomOverall *= zoom
+ if (zoomOverall >= 1f) {
+ zoomOverall = 1f
+ } else if (zoomOverall <= 0.1f) {
+ zoomOverall = 0.1f
+ }
+ Log.d(
+ TAG,
+ "GamePage: zoom = $zoomOverall"
+ )
+ },
+ )
+ .padding(innerPadding)
+ .padding(100.dp),
+ ) {
+ for (i in gameState.matrix.indices) {
+ val items = gameState.matrix[i]
+ Row {
+ for (j in items.indices) {
+ val cell = items[j]
+ Cell(
+ cell = cell,
+ zoom = zoomOverall,
+ onSelect = {
+ if(isLost || isWon){
+ return@Cell
+ }
+ when (action) {
+ Action.OPEN -> {
+ if (cell.isFlag) {
+ return@Cell
+ }
+ if (cell.isBomb) {
+ cell.isOpened = true
+ } else if (cell.countAround == 0) {
+ gameState.openNearbyCells(
+ i,
+ j,
+ )
+ } else {
+ cell.isOpened = true
+ }
+ }
+
+ Action.FLAG -> {
+ if (cell.isOpened) {
+ return@Cell
+ }
+ cell.isFlag = !cell.isFlag
+ }
+ }
+ },
+ )
+ }
+ }
+ }
+ }
+ ActionChooser(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(innerPadding)
+ .padding(bottom = 20.dp),
+ selected = action,
+ onSelect = {
+ action = it
+ }
+ )
+ }
+ }
+
+ var dialogIsDismissed by remember { mutableStateOf(false) }
+
+ if(!dialogIsDismissed) {
+ if (isLost) {
+ Dialog(
+ onDismissRequest = {
+ dialogIsDismissed = true
+ },
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth().background(
+ color = MaterialTheme.colorScheme.background,
+ shape = MaterialTheme.shapes.large,
+ ).padding(10.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ text = "YOU LOST!",
+ textAlign = TextAlign.Center,
+ )
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ text = "Good luck on your next game.",
+ textAlign = TextAlign.Center,
+ )
+ ElevatedButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ onClick = onNewGame,
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = "New Game",
+ textAlign = TextAlign.Center,
+ )
+ }
+ ElevatedButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ onClick = onBack,
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = "Back",
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+ }
+ } else if (isWon) {
+ Dialog(
+ onDismissRequest = {
+ dialogIsDismissed = true
+ },
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth().background(
+ color = MaterialTheme.colorScheme.background,
+ shape = MaterialTheme.shapes.large,
+ ).padding(10.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ text = "YOU WON!",
+ textAlign = TextAlign.Center,
+ )
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ text = "You found ${gameState.remainingBombs} mines in ${gameState.minutes}:${
+ gameState.seconds.toString().padStart(
+ 2,
+ '0'
+ )
+ }",
+ textAlign = TextAlign.Center,
+ )
+ ElevatedButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ onClick = onNewGame,
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = "New Game",
+ textAlign = TextAlign.Center,
+ )
+ }
+ ElevatedButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 10.dp),
+ onClick = onBack,
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = "Back",
+ textAlign = TextAlign.Center,
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun LabeledIcon(
+ modifier: Modifier = Modifier,
+ @DrawableRes icon: Int,
+ contentDescription: String?,
+ text: String,
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier
+ .padding(end = 8.dp)
+ .size(24.dp),
+ painter = painterResource(icon),
+ contentDescription = contentDescription,
+ )
+ Text(
+ modifier = Modifier,
+ text = text,
+ style = MaterialTheme.typography.titleSmall,
+ )
+ }
+}
+
+@Composable
+fun Cell(
+ modifier: Modifier = Modifier,
+ cell: GameState.Cell,
+ zoom: Float,
+ onSelect: () -> Unit,
+) {
+ Box(
+ modifier = modifier
+ .size(50.dp * zoom)
+ .background(
+ color =
+ if (cell.isOpened) Color.Transparent
+ else MaterialTheme.colorScheme.primary,
+ shape = RoundedCornerShape(0),
+ )
+ .let {
+ if (!cell.isOpened) {
+ it.clickable {
+ onSelect()
+ }
+ } else {
+ it
+ }
+ }
+ .padding(8.dp * zoom),
+ contentAlignment = Alignment.Center,
+ ) {
+ if (cell.isOpened) {
+ if (cell.isBomb) {
+ Icon(
+ modifier = Modifier.fillMaxSize(),
+ painter = painterResource(R.drawable.mine),
+ contentDescription = "Mine",
+ )
+ } else if (cell.countAround != 0) {
+ Text(
+ text = cell.countAround.toString(),
+ style = MaterialTheme.typography.titleSmall.copy(
+ fontSize = MaterialTheme.typography.titleSmall.fontSize * zoom,
+ ),
+ lineHeight = 0.0001.sp,
+ )
+ }
+ } else {
+ if (cell.isFlag) {
+ Icon(
+ modifier = Modifier.fillMaxSize(),
+ painter = painterResource(R.drawable.flag),
+ contentDescription = "Flag",
+ )
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/page/Home.kt b/app/src/main/java/com/a404m/mine_game/ui/page/Home.kt
new file mode 100644
index 0000000..2a97c01
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/page/Home.kt
@@ -0,0 +1,286 @@
+package com.a404m.mine_game.ui.page
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.a404m.mine_game.R
+import com.a404m.mine_game.model.GameState
+import com.a404m.mine_game.ui.utils.CustomScaffold
+import com.a404m.mine_game.utils.getApplicationName
+
+@Composable
+fun HomePage(
+ modifier: Modifier = Modifier,
+ onGoToContinue: () -> Unit,
+ onGoToNewGame: () -> Unit,
+ onGoToThemes: () -> Unit,
+ onGoToControls: () -> Unit,
+ onGoToSettings: () -> Unit,
+ onGoToStatistics: () -> Unit,
+ onGoToPreviousGames: () -> Unit,
+ onGoToDonation: () -> Unit,
+ onGoToTutorial: () -> Unit,
+ onGoToLanguage: () -> Unit,
+ onGoToAbout: () -> Unit,
+) {
+ val hasLastGame by remember { derivedStateOf { GameState.current != null } }
+ CustomScaffold(
+ modifier = modifier.fillMaxSize(),
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .align(Alignment.Center)
+ .padding(innerPadding)
+ .padding(horizontal = 30.dp)
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.Start,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ LogoAndName(
+ modifier = Modifier
+ .padding(bottom = 10.dp)
+ .fillMaxWidth(),
+ color = Color(0xff303030),
+ )
+ MenuGroup(
+ modifier = Modifier
+ .padding(bottom = 10.dp)
+ .fillMaxWidth(),
+ items = if (hasLastGame) listOf(
+ MenuItem(
+ icon = R.drawable.play,
+ text = "Continue",
+ onClick = onGoToContinue,
+ isPrimary = true,
+ ),
+ MenuItem(
+ icon = R.drawable.add,
+ text = "New Game",
+ onClick = onGoToNewGame,
+ ),
+ ) else listOf(
+ MenuItem(
+ icon = R.drawable.add,
+ text = "New Game",
+ onClick = onGoToNewGame,
+ ),
+ ),
+ )
+ MenuGroup(
+ modifier = Modifier
+ .padding(bottom = 10.dp)
+ .fillMaxWidth(),
+ items = listOf(
+ MenuItem(
+ icon = R.drawable.theme,
+ text = "Themes",
+ onClick = onGoToThemes,
+ ),
+ MenuItem(
+ icon = R.drawable.controls,
+ text = "Controls",
+ onClick = onGoToControls,
+ ),
+ MenuItem(
+ icon = R.drawable.settings,
+ text = "Settings",
+ onClick = onGoToSettings,
+ ),
+ ),
+ )
+ MenuGroup(
+ modifier = Modifier
+ .padding(bottom = 10.dp)
+ .fillMaxWidth(),
+ items = listOf(
+ MenuItem(
+ icon = R.drawable.statistics,
+ text = "Statistics",
+ onClick = onGoToStatistics,
+ ),
+ MenuItem(
+ icon = R.drawable.history,
+ text = "Previous Games",
+ onClick = onGoToPreviousGames,
+ ),
+ ),
+ )
+ MenuGroup(
+ modifier = Modifier
+ .padding(bottom = 10.dp)
+ .fillMaxWidth(),
+ items = listOf(
+ MenuItem(
+ icon = R.drawable.donate,
+ text = "Donation",
+ onClick = onGoToDonation,
+ ),
+ MenuItem(
+ icon = R.drawable.tutorial,
+ text = "Tutorial",
+ onClick = onGoToTutorial,
+ ),
+ MenuItem(
+ icon = R.drawable.language,
+ text = "Language",
+ onClick = onGoToLanguage,
+ ),
+ MenuItem(
+ icon = R.drawable.about_us,
+ text = "About",
+ onClick = onGoToAbout,
+ ),
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+fun LogoAndName(
+ modifier: Modifier = Modifier,
+ color: Color,
+) {
+ val context = LocalContext.current
+ val density = LocalDensity.current
+
+ var height by remember { mutableStateOf(0.dp) }
+
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier
+ .padding(end = 2.dp)
+ .size(height)
+ .padding(bottom = 2.dp),
+ painter = painterResource(R.drawable.mine),
+ contentDescription = "Logo",
+ tint = color,
+ )
+ Text(
+ modifier = Modifier.onGloballyPositioned {
+ with(density) {
+ height = it.size.height.toDp()
+ }
+ },
+ text = getApplicationName(context),
+ style = MaterialTheme.typography.titleLarge,
+ color = color,
+ fontWeight = FontWeight.SemiBold,
+ lineHeight = 0.01.sp,
+ maxLines = 1,
+ minLines = 1,
+ )
+ }
+}
+
+data class MenuItem(
+ @DrawableRes val icon: Int,
+ val text: String,
+ val onClick: () -> Unit,
+ val isPrimary: Boolean = false,
+)
+
+@Composable
+fun MenuGroup(
+ modifier: Modifier = Modifier,
+ items: List<MenuItem>,
+) {
+ val density = LocalDensity.current
+
+ Column(
+ modifier = modifier
+ .background(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = MaterialTheme.shapes.medium,
+ )
+ .padding(4.dp),
+ ) {
+ for (i in items.indices) {
+ val item = items[i]
+ val backgroundColor =
+ if (item.isPrimary) MaterialTheme.colorScheme.primary
+ else Color.Transparent
+ val contentColor =
+ if (item.isPrimary) MaterialTheme.colorScheme.contentColorFor(backgroundColor)
+ else MaterialTheme.colorScheme.primary
+
+ if (i != 0) {
+ Spacer(
+ modifier = Modifier.height(4.dp),
+ )
+ }
+ Row(
+ modifier = Modifier
+ .background(
+ color = backgroundColor,
+ shape = MaterialTheme.shapes.small,
+ )
+ .clip(
+ shape = MaterialTheme.shapes.small,
+ )
+ .fillMaxWidth()
+ .clickable {
+ item.onClick()
+ }
+ .padding(10.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier
+ .padding(end = 5.dp)
+ .size(25.dp),
+ painter = painterResource(item.icon),
+ contentDescription = "Icon",
+ tint = contentColor,
+ )
+ Text(
+ modifier = Modifier,
+ text = item.text.uppercase(),
+ style = MaterialTheme.typography.titleMedium,
+ color = contentColor,
+ lineHeight = 0.01.sp,
+ maxLines = 1,
+ minLines = 1,
+ )
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/page/Route.kt b/app/src/main/java/com/a404m/mine_game/ui/page/Route.kt
new file mode 100644
index 0000000..f622952
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/page/Route.kt
@@ -0,0 +1,166 @@
+package com.a404m.mine_game.ui.page
+
+import android.util.Log
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navArgument
+import com.a404m.mine_game.core.TAG
+import com.a404m.mine_game.model.GameSettings
+import com.a404m.mine_game.model.GameState
+import com.a404m.mine_game.storage.StorageGame
+
+@Composable
+fun Route(
+ modifier: Modifier = Modifier,
+ navController: NavHostController = rememberNavController(),
+) {
+ NavHost(
+ modifier = modifier,
+ navController = navController,
+ startDestination = AppRoute.getSplashRoute(),
+ ) {
+ composable(
+ route = AppRoute.getSplashStaticRoute(),
+ ) {
+ Log.d(
+ TAG,
+ "Route: Hey"
+ )
+ SplashPage(
+ onGoToHome = {
+ navController.navigate(
+ AppRoute.getHomeRoute(),
+ ) {
+ popUpTo(navController.graph.findStartDestination().id) {
+ inclusive = true
+ }
+ }
+ },
+ )
+ }
+ composable(
+ route = AppRoute.getHomeStaticRoute(),
+ ) {
+ HomePage(
+ onGoToContinue = {
+ navController.navigate(AppRoute.getGameRoute(newGame = false))
+ },
+ onGoToNewGame = {
+ navController.navigate(AppRoute.getGameRoute(newGame = true))
+ },
+ onGoToThemes = {},
+ onGoToControls = {
+ navController.navigate(AppRoute.getControlsRoute())
+ },
+ onGoToSettings = {
+ navController.navigate(AppRoute.getSettingsRoute())
+ },
+ onGoToStatistics = {},
+ onGoToPreviousGames = {},
+ onGoToDonation = {},
+ onGoToTutorial = {},
+ onGoToLanguage = {},
+ onGoToAbout = {}
+ )
+ }
+ composable(
+ route = AppRoute.getGameStaticRoute(),
+ arguments = AppRoute.getGameArguments(),
+ ) {
+ val newGame = it.arguments!!.getBoolean("new_game")
+
+ val gameState = remember {
+ if (newGame) {
+ val gameSettings = GameSettings.current
+ GameState(
+ width = gameSettings.width,
+ height = gameSettings.height,
+ mines = gameSettings.mines,
+ seed = gameSettings.seed,
+ )
+ } else {
+ StorageGame.getLastGame()!!
+ }
+ }
+
+ GameState.current = gameState
+
+ GamePage(
+ onBack = {
+ navController.popBackStack()
+ },
+ onNewGame = {
+ navController.navigate(AppRoute.getGameRoute(true)){
+ popUpTo(navController.graph.findStartDestination().id){
+ inclusive = true
+ }
+ }
+ },
+ gameState = gameState,
+ )
+ }
+ composable(
+ route = AppRoute.getControlsStaticRoute(),
+ ) {
+ ControlsPage(
+ onBack = {
+ navController.popBackStack()
+ },
+ )
+ }
+ composable(
+ route = AppRoute.getSettingsStaticRoute(),
+ ) {
+ SettingsPage(
+ onBack = {
+ navController.popBackStack()
+ },
+ )
+ }
+ }
+}
+
+object AppRoute {
+ private const val SPLASH = "splash"
+ private const val HOME = "home"
+ private const val GAME = "game"
+ private const val CONTROLS = "controls"
+ private const val SETTINGS = "settings"
+
+ fun getSplashStaticRoute() = "/$SPLASH"
+ fun getSplashRoute() = "/$SPLASH"
+
+ fun getHomeStaticRoute() = "/$HOME"
+ fun getHomeRoute() = "/$HOME"
+
+ fun getGameStaticRoute() = "/$GAME/{new_game}"
+ fun getGameRoute(newGame: Boolean) = "/$GAME/$newGame"
+ fun getGameArguments() = listOf(
+ navArgument("new_game") {
+ type = NavType.BoolType
+ },
+ )
+
+ fun getControlsStaticRoute() = "/$CONTROLS"
+ fun getControlsRoute() = "/$CONTROLS"
+
+ fun getSettingsStaticRoute() = "/$SETTINGS"
+ fun getSettingsRoute() = "/$SETTINGS"
+
+ /*
+ fun getChatStaticRoute() = "/$CHAT/{chat_id}"
+ fun getChatRoute(chat: Chat) = "/$CHAT/${chat.id}"
+ fun getChatArguments() = listOf(
+ navArgument("chat_id") {
+ type = NavType.IntType
+ },
+ )
+ */
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/page/Settings.kt b/app/src/main/java/com/a404m/mine_game/ui/page/Settings.kt
new file mode 100644
index 0000000..09c07f1
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/page/Settings.kt
@@ -0,0 +1,259 @@
+package com.a404m.mine_game.ui.page
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.max
+import androidx.compose.ui.unit.sp
+import com.a404m.mine_game.R
+import com.a404m.mine_game.model.GameSettings
+import com.a404m.mine_game.storage.StorageGame
+import com.a404m.mine_game.ui.utils.CustomScaffold
+import com.a404m.mine_game.ui.utils.TitledColumn
+import com.a404m.mine_game.ui.utils.TopBar
+import com.a404m.mine_game.ui.utils.showToast
+
+@Composable
+fun SettingsPage(
+ modifier: Modifier = Modifier,
+ onBack: () -> Unit,
+) {
+ var width by rememberSaveable { mutableStateOf(GameSettings.current.width.toString()) }
+ var height by rememberSaveable { mutableStateOf(GameSettings.current.height.toString()) }
+ var mines by rememberSaveable { mutableStateOf(GameSettings.current.mines.toString()) }
+ var seed by rememberSaveable { mutableStateOf(GameSettings.current.seed?.toString() ?: "") }
+
+ val isWidthValid by remember {
+ derivedStateOf {
+ val width = width.toIntOrNull()
+ (width?:0) >= 1
+ }
+ }
+ val isHeightValid by remember {
+ derivedStateOf {
+ val height = height.toIntOrNull()
+ (height?:0) >= 1
+ }
+ }
+ val isMinesValid by remember {
+ derivedStateOf {
+ val mines = mines.toIntOrNull() ?: return@derivedStateOf false
+
+ val width = width.toIntOrNull() ?: return@derivedStateOf true
+ val height = height.toIntOrNull() ?: return@derivedStateOf true
+
+ mines in 1 until (width * height)
+ }
+ }
+ val isSeedValid by remember { derivedStateOf { seed.isEmpty() || seed.toIntOrNull() != null } }
+
+ CustomScaffold(
+ modifier = modifier.fillMaxSize(),
+ topBar = { innerPadding ->
+ TopBar(
+ modifier = Modifier.padding(innerPadding),
+ title = "Settings",
+ onBack = onBack,
+ actions = {
+ IconButton(
+ onClick = {
+ GameSettings.current = GameSettings.default
+ width = GameSettings.current.width.toString()
+ height = GameSettings.current.height.toString()
+ mines = GameSettings.current.mines.toString()
+ seed = GameSettings.current.seed?.toString() ?: ""
+ StorageGame.deleteGameSettings()
+ },
+ ) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(R.drawable.delete),
+ contentDescription = "Delete to default settings",
+ )
+ }
+ IconButton(
+ onClick = {
+ if (!isWidthValid ||
+ !isHeightValid ||
+ !isMinesValid ||
+ !isSeedValid
+ ) {
+ showToast("Fix the errors")
+ return@IconButton
+ }
+ val settings = GameSettings(
+ width = width.toInt(),
+ height = height.toInt(),
+ mines = mines.toInt(),
+ seed = seed.toIntOrNull(),
+ )
+ GameSettings.current = settings
+ StorageGame.save(settings)
+ },
+ ) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(R.drawable.save),
+ contentDescription = "Save settings",
+ )
+ }
+ },
+ )
+ }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(innerPadding)
+ .padding(10.dp),
+ ) {
+ TitledColumn(
+ modifier = Modifier
+ .background(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = MaterialTheme.shapes.medium,
+ ),
+ title = "New Game",
+ ) {
+ val density = LocalDensity.current
+ var spaceBetween by remember { mutableStateOf(0.dp) }
+ Row(
+ modifier = Modifier.padding(
+ start = 20.dp,
+ end = 20.dp,
+ bottom = max(
+ 0.dp,
+ spaceBetween - 8.dp
+ ),
+ ),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .weight(1F),
+ value = width,
+ onValueChange = {
+ width = it
+ },
+ isError = !isWidthValid,
+ label = {
+ Text(
+ text = "Width",
+ )
+ },
+ keyboardOptions = KeyboardOptions.Default.copy(
+ keyboardType = KeyboardType.Number,
+ ),
+ )
+ Text(
+ modifier = Modifier
+ .onGloballyPositioned {
+ with(density) {
+ spaceBetween = it.size.width.toDp()
+ }
+ }
+ .padding(top = 6.dp)
+ .padding(horizontal = 2.dp),
+ text = "X",
+ style = MaterialTheme.typography.labelSmall,
+ lineHeight = 0.01.sp,
+ )
+ OutlinedTextField(
+ modifier = Modifier
+ .weight(1F),
+ value = height,
+ onValueChange = {
+ height = it
+ },
+ isError = !isHeightValid,
+ label = {
+ Text(
+ text = "Height",
+ )
+ },
+ keyboardOptions = KeyboardOptions.Default.copy(
+ keyboardType = KeyboardType.Number,
+ ),
+ )
+ }
+ Row(
+ modifier = Modifier.padding(
+ start = 20.dp,
+ end = 20.dp,
+ bottom = 20.dp
+ ),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .weight(1F),
+ value = mines,
+ onValueChange = {
+ mines = it
+ },
+ isError = !isMinesValid,
+ label = {
+ Text(
+ text = "Mines",
+ )
+ },
+ keyboardOptions = KeyboardOptions.Default.copy(
+ keyboardType = KeyboardType.Number,
+ ),
+ )
+ Spacer(
+ modifier = Modifier.width(spaceBetween),
+ )
+ OutlinedTextField(
+ modifier = Modifier
+ .weight(1F),
+ value = seed,
+ onValueChange = {
+ seed = it
+ },
+ isError = !isSeedValid,
+ label = {
+ Text(
+ text = "Seed",
+ )
+ },
+ keyboardOptions = KeyboardOptions.Default.copy(
+ keyboardType = KeyboardType.Number,
+ ),
+ )
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/page/Splash.kt b/app/src/main/java/com/a404m/mine_game/ui/page/Splash.kt
new file mode 100644
index 0000000..7db68f6
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/page/Splash.kt
@@ -0,0 +1,38 @@
+package com.a404m.mine_game.ui.page
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.a404m.mine_game.model.Action
+import com.a404m.mine_game.model.GameSettings
+import com.a404m.mine_game.model.GameState
+import com.a404m.mine_game.storage.StorageGame
+import com.a404m.mine_game.ui.utils.CustomScaffold
+
+@Composable
+fun SplashPage(
+ modifier: Modifier = Modifier,
+ onGoToHome: () -> Unit,
+) {
+ LaunchedEffect(Unit) {
+ GameSettings.current = StorageGame.getGameSettings() ?: GameSettings.default
+ GameState.current = StorageGame.getLastGame()
+ Action.current = StorageGame.getPrimaryAction() ?: Action.OPEN
+ onGoToHome()
+ }
+
+ CustomScaffold(
+ modifier = modifier.fillMaxSize(),
+ ) { innerPadding ->
+ Text(
+ modifier = Modifier
+ .padding(innerPadding)
+ .align(Alignment.Center),
+ text = "Loading..."
+ )
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/theme/Color.kt b/app/src/main/java/com/a404m/mine_game/ui/theme/Color.kt
new file mode 100644
index 0000000..c414f46
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.a404m.mine_game.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
diff --git a/app/src/main/java/com/a404m/mine_game/ui/theme/Font.kt b/app/src/main/java/com/a404m/mine_game/ui/theme/Font.kt
new file mode 100644
index 0000000..d35fab3
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/theme/Font.kt
@@ -0,0 +1,27 @@
+package com.a404m.mine_game.ui.theme
+
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import com.a404m.mine_game.R
+
+object AppFont {
+ val IranYekan = FontFamily(
+ Font(R.font.iranyekan_bold, weight = FontWeight.Bold),
+ Font(R.font.iranyekan_medium, weight = FontWeight.Medium),
+ Font(R.font.iranyekan_regular, weight = FontWeight.Normal),
+ Font(R.font.iranyekan_light, weight = FontWeight.Light),
+ )
+ val VazirMatn = FontFamily(
+ Font(R.font.vazirmatn),
+ Font(R.font.vazirmatn_black, weight = FontWeight.Black),
+ Font(R.font.vazirmatn_extrabold, weight = FontWeight.ExtraBold),
+ Font(R.font.vazirmatn_bold, weight = FontWeight.Bold),
+ Font(R.font.vazirmatn_semibold, weight = FontWeight.SemiBold),
+ Font(R.font.vazirmatn_medium, weight = FontWeight.Medium),
+ Font(R.font.vazirmatn_regular, weight = FontWeight.Normal),
+ Font(R.font.vazirmatn_light, weight = FontWeight.Light),
+ Font(R.font.vazirmatn_extralight, weight = FontWeight.ExtraLight),
+ Font(R.font.vazirmatn_thin, weight = FontWeight.Thin),
+ )
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/theme/Theme.kt b/app/src/main/java/com/a404m/mine_game/ui/theme/Theme.kt
new file mode 100644
index 0000000..20aa623
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/theme/Theme.kt
@@ -0,0 +1,82 @@
+package com.a404m.mine_game.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Typography
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val defaultTypography = Typography()
+private val typography = defaultTypography
+ /* Typography(
+ displayLarge = defaultTypography.displayLarge.copy(fontFamily = AppFont.VazirMatn),
+ displayMedium = defaultTypography.displayMedium.copy(fontFamily = AppFont.VazirMatn),
+ displaySmall = defaultTypography.displaySmall.copy(fontFamily = AppFont.VazirMatn),
+
+ headlineLarge = defaultTypography.headlineLarge.copy(fontFamily = AppFont.VazirMatn),
+ headlineMedium = defaultTypography.headlineMedium.copy(fontFamily = AppFont.VazirMatn),
+ headlineSmall = defaultTypography.headlineSmall.copy(fontFamily = AppFont.VazirMatn),
+
+ titleLarge = defaultTypography.titleLarge.copy(fontFamily = AppFont.VazirMatn),
+ titleMedium = defaultTypography.titleMedium.copy(fontFamily = AppFont.VazirMatn),
+ titleSmall = defaultTypography.titleSmall.copy(fontFamily = AppFont.VazirMatn),
+
+ bodyLarge = defaultTypography.bodyLarge.copy(fontFamily = AppFont.VazirMatn),
+ bodyMedium = defaultTypography.bodyMedium.copy(fontFamily = AppFont.VazirMatn),
+ bodySmall = defaultTypography.bodySmall.copy(fontFamily = AppFont.VazirMatn),
+
+ labelLarge = defaultTypography.labelLarge.copy(fontFamily = AppFont.VazirMatn),
+ labelMedium = defaultTypography.labelMedium.copy(fontFamily = AppFont.VazirMatn),
+ labelSmall = defaultTypography.labelSmall.copy(fontFamily = AppFont.VazirMatn),
+)
+ */
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ surface = Color(0xFFFFFBFE),
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun MineGameTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = typography,
+ content = {
+ content()
+ },
+ )
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/theme/Type.kt b/app/src/main/java/com/a404m/mine_game/ui/theme/Type.kt
new file mode 100644
index 0000000..900ff40
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.a404m.mine_game.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+) \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/utils/Action.kt b/app/src/main/java/com/a404m/mine_game/ui/utils/Action.kt
new file mode 100644
index 0000000..3e06a0a
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/utils/Action.kt
@@ -0,0 +1,62 @@
+package com.a404m.mine_game.ui.utils
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.a404m.mine_game.model.Action
+
+@Composable
+fun ActionChooser(
+ modifier: Modifier = Modifier,
+ selected: Action,
+ onSelect: (Action) -> Unit,
+) {
+ val contentColor = MaterialTheme.colorScheme.primary
+ val backgroundColor = MaterialTheme.colorScheme.onPrimary
+ Row(
+ modifier = modifier
+ .background(
+ color = backgroundColor,
+ shape = MaterialTheme.shapes.small,
+ )
+ .border(
+ width = 2.dp,
+ color = contentColor,
+ shape = MaterialTheme.shapes.small,
+ ),
+ ) {
+ for (action in Action.entries) {
+ val isSelected = action == selected
+ Icon(
+ modifier = Modifier
+ .background(
+ color = if (isSelected) contentColor else backgroundColor,
+ shape = MaterialTheme.shapes.small,
+ )
+ .clip(
+ shape = MaterialTheme.shapes.small,
+ )
+ .clickable {
+ onSelect(action)
+ }
+ .padding(10.dp)
+ .size(25.dp),
+ painter = painterResource(action.icon),
+ contentDescription = action.name,
+ tint =
+ if (isSelected) backgroundColor
+ else contentColor,
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/utils/AppBar.kt b/app/src/main/java/com/a404m/mine_game/ui/utils/AppBar.kt
new file mode 100644
index 0000000..bf15d24
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/utils/AppBar.kt
@@ -0,0 +1,54 @@
+package com.a404m.mine_game.ui.utils
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.a404m.mine_game.R
+
+@Composable
+fun TopBar(
+ modifier: Modifier = Modifier,
+ onBack: () -> Unit,
+ title: String,
+ actions: @Composable RowScope.() -> Unit = {},
+) {
+ Row(
+ modifier = modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ IconButton(
+ modifier = Modifier.padding(end = 10.dp),
+ onClick = onBack,
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.arrow_back),
+ contentDescription = "Back",
+ )
+ }
+ Text(
+ modifier = Modifier
+ .padding(end = 10.dp),
+ text = title,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ Row(
+ modifier = Modifier
+ .weight(1F),
+ horizontalArrangement = Arrangement.End,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ actions()
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/utils/Container.kt b/app/src/main/java/com/a404m/mine_game/ui/utils/Container.kt
new file mode 100644
index 0000000..e40c42f
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/utils/Container.kt
@@ -0,0 +1,160 @@
+package com.a404m.mine_game.ui.utils
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.add
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.isImeVisible
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun CustomScaffold(
+ modifier: Modifier = Modifier,
+ topBar: (@Composable (PaddingValues) -> Unit)? = null,
+ bottomBar: (@Composable (PaddingValues) -> Unit)? = null,
+ content: @Composable BoxScope.(PaddingValues) -> Unit,
+) {
+ val density = LocalDensity.current
+ var topBarHeight by remember { mutableStateOf(0.dp) }
+
+ Box(
+ modifier = modifier
+ .background(
+ color = MaterialTheme.colorScheme.background,
+ )
+ .imePadding(),
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ ) {
+ content(
+ calcContentWindowInsets(
+ isTopBarVisible = topBar != null,
+ isBottomBarVisible = bottomBar != null,
+ topBarInsets = WindowInsets(
+ top = topBarHeight,
+ ),
+ ).asPaddingValues()
+ )
+ }
+ if (topBar != null) {
+ Box(
+ modifier = Modifier.onGloballyPositioned {
+ with(density) {
+ topBarHeight = it.size.height.toDp()
+ }
+ },
+ ) {
+ topBar(
+ WindowInsets.statusBars.asPaddingValues()
+ )
+ }
+ }
+ if (bottomBar != null)
+ bottomBar(
+ calcBottomBarPadding().asPaddingValues()
+ )
+ }
+}
+
+@Composable
+private fun calcContentWindowInsets(
+ isTopBarVisible: Boolean,
+ isBottomBarVisible: Boolean,
+ topBarInsets: WindowInsets,
+): WindowInsets {
+ return if (isTopBarVisible) {
+ if (isBottomBarVisible) {
+ topBarInsets
+ } else {
+ calcBottomBarPadding().add(topBarInsets)
+ }
+ } else if (isBottomBarVisible) {
+ WindowInsets.statusBars
+ } else {
+ WindowInsets.statusBars.add(calcBottomBarPadding())
+ }
+}
+
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+private fun calcBottomBarPadding(): WindowInsets {
+ val bottomBar = WindowInsets.navigationBars
+ return if (WindowInsets.isImeVisible) {
+ val density = LocalDensity.current
+ val direction = LocalLayoutDirection.current
+
+ val imePadding = WindowInsets.ime
+ if (bottomBar.getBottom(density) >= imePadding.getBottom(density)) {
+ WindowInsets(
+ left = bottomBar.getLeft(
+ density,
+ direction,
+ ),
+ right = bottomBar.getRight(
+ density,
+ direction,
+ ),
+ top = bottomBar.getTop(density),
+ bottom = bottomBar.getBottom(density) - imePadding.getBottom(density),
+ )
+ } else {
+ WindowInsets(
+ left = 0.dp,
+ right = 0.dp,
+ top = 0.dp,
+ bottom = 0.dp,
+ )
+ }
+ } else {
+ bottomBar
+ }
+}
+
+@Composable
+fun TitledColumn(
+ modifier: Modifier = Modifier,
+ title: String,
+ content: @Composable ColumnScope.() -> Unit
+) {
+ Column(
+ modifier = modifier.padding(5.dp),
+ ) {
+ Text(
+ modifier = Modifier.padding(
+ top = 10.dp,
+ start = 15.dp,
+ end = 15.dp,
+ bottom = 5.dp,
+ ),
+ text = title.uppercase(),
+ style = MaterialTheme.typography.titleSmall,
+ )
+ content()
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/utils/Dialog.kt b/app/src/main/java/com/a404m/mine_game/ui/utils/Dialog.kt
new file mode 100644
index 0000000..6b1a552
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/utils/Dialog.kt
@@ -0,0 +1,47 @@
+package com.a404m.mine_game.ui.utils
+
+import android.view.ViewGroup
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.window.Dialog
+import com.a404m.mine_game.ContextHelper
+import com.a404m.mine_game.ui.theme.MineGameTheme
+
+fun showDialog(
+ canDismiss: Boolean = true,
+ onDismiss: (byUser: Boolean) -> Unit = {},
+ content: @Composable (dismiss: () -> Unit) -> Unit,
+) {
+ ContextHelper.context.addContentView(
+ ComposeView(ContextHelper.context).apply {
+ setContent {
+ var showDialog by remember { mutableStateOf(true) }
+ if (showDialog) {
+ MineGameTheme {
+ Dialog(
+ onDismissRequest = {
+ if (canDismiss) {
+ onDismiss(true)
+ showDialog = false
+ }
+ }
+ ) {
+ content {
+ onDismiss(false)
+ showDialog = false
+ }
+ }
+ }
+ }
+ }
+ },
+ ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ )
+ )
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/utils/Dropdown.kt b/app/src/main/java/com/a404m/mine_game/ui/utils/Dropdown.kt
new file mode 100644
index 0000000..fb8887f
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/utils/Dropdown.kt
@@ -0,0 +1,86 @@
+package com.a404m.mine_game.ui.utils
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.a404m.mine_game.R
+import com.a404m.mine_game.ui.theme.MineGameTheme
+
+@Composable
+fun <T> Dropdown(
+ modifier: Modifier = Modifier,
+ items: List<T>,
+ selectedItem: T?,
+ onItemChange: (T) -> Unit,
+ hint: String = "",
+ toString: (T) -> String = { it.toString() },
+) {
+ var expanded by remember { mutableStateOf(false) }
+ Box {
+ Row(
+ modifier = modifier
+ .clickable { expanded = true }
+ .padding(5.dp),
+ ) {
+ Text(
+ modifier = Modifier
+ .weight(1F)
+ .padding(end = 5.dp),
+ text = if (selectedItem != null) toString(selectedItem) else hint
+ )
+ Icon(
+ modifier = Modifier.size(24.dp),
+ painter =
+ if (expanded) painterResource(R.drawable.arrow_drop_up)
+ else painterResource(R.drawable.arrow_drop_down),
+ contentDescription = "Drop down",
+ )
+ }
+ DropdownMenu(
+ modifier = Modifier.fillMaxWidth(),
+ expanded = expanded,
+ onDismissRequest = {
+ expanded = false
+ },
+ ) {
+ for (item in items) {
+ MineGameTheme {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .apply {
+ if (item == selectedItem) {
+ background(
+ color = MaterialTheme.colorScheme.primary,
+ )
+ }
+ }
+ .clickable {
+ onItemChange(item)
+ expanded = false
+ }
+ .padding(5.dp)
+ ,
+ text = toString(item),
+ )
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/utils/Toast.kt b/app/src/main/java/com/a404m/mine_game/ui/utils/Toast.kt
new file mode 100644
index 0000000..6ac84f0
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/utils/Toast.kt
@@ -0,0 +1,15 @@
+package com.a404m.mine_game.ui.utils
+
+import android.widget.Toast
+import com.a404m.mine_game.ContextHelper
+
+
+fun showToast(text: String) {
+ ContextHelper.context.runOnUiThread{
+ Toast.makeText(
+ ContextHelper.context,
+ text,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/utils/modifier/Extension.kt b/app/src/main/java/com/a404m/mine_game/ui/utils/modifier/Extension.kt
new file mode 100644
index 0000000..9401a62
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/utils/modifier/Extension.kt
@@ -0,0 +1,323 @@
+package com.a404m.mine_game.ui.utils.modifier
+
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.calculateCentroid
+import androidx.compose.foundation.gestures.calculateCentroidSize
+import androidx.compose.foundation.gestures.calculatePan
+import androidx.compose.foundation.gestures.calculateRotation
+import androidx.compose.foundation.gestures.calculateZoom
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerId
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChanged
+import androidx.compose.ui.input.pointer.util.VelocityTracker
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastForEach
+import com.a404m.mine_game.ui.utils.modifier.model.FreeScrollState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlin.math.PI
+import kotlin.math.abs
+
+
+/**
+ * Modify element to allow to scroll in both directions.
+ *
+ * Note that this modifier uses [pointerInput] as the underlying implementation, so some
+ * pointer events will be consumed. If you want to use
+ * [androidx.compose.foundation.gestures.detectTransformGestures] simultaneously,
+ * use [freeScrollWithTransformGesture] instead.
+ *
+ * @param state state of the scroll
+ * @param enabled whether the scroll is enabled
+ * @param horizontalReverseScrolling reverse the horizontal scrolling direction,
+ * when true, 0 [FreeScrollState.xValue] will mean right, otherwise left.
+ * @param verticalReverseScrolling reverse the vertical scrolling direction,
+ * when true, 0 [FreeScrollState.yValue] will mean bottom, otherwise top.
+ * @param flingBehavior logic describing fling behavior when drag has finished with velocity.
+ * If null, default from [ScrollableDefaults.flingBehavior] will be used.
+ */
+fun Modifier.freeScroll(
+ state: FreeScrollState,
+ enabled: Boolean = true,
+ horizontalReverseScrolling: Boolean = false,
+ verticalReverseScrolling: Boolean = false,
+ flingBehavior: FlingBehavior? = null,
+): Modifier = composed {
+
+ val velocityTracker = remember { VelocityTracker() }
+ val fling = flingBehavior ?: ScrollableDefaults.flingBehavior()
+
+ this.horizontalScroll(
+ state = state.horizontalScrollState,
+ enabled = false,
+ reverseScrolling = horizontalReverseScrolling
+ ).verticalScroll(
+ state = state.verticalScrollState,
+ enabled = false,
+ reverseScrolling = verticalReverseScrolling
+ )
+ .pointerInput(enabled, horizontalReverseScrolling, verticalReverseScrolling) {
+ if (!enabled) return@pointerInput
+
+ coroutineScope {
+ detectDragGestures(
+ onDragStart = { },
+ onDrag = { change, dragAmount ->
+ change.consume()
+ onDrag(
+ change = change,
+ dragAmount = dragAmount,
+ state = state,
+ horizontalReverseScrolling = horizontalReverseScrolling,
+ verticalReverseScrolling = verticalReverseScrolling,
+ velocityTracker = velocityTracker,
+ coroutineScope = this
+ )
+ },
+ onDragEnd = {
+ onEnd(
+ velocityTracker = velocityTracker,
+ state = state,
+ horizontalReverseScrolling = horizontalReverseScrolling,
+ verticalReverseScrolling = verticalReverseScrolling,
+ flingBehavior = fling,
+ coroutineScope = this
+ )
+ }
+ )
+ }
+ }
+}
+
+/**
+ * Modify element to allow to scroll in both directions, and detect transform gestures.
+ * If you don't need to detect transform gestures, use [freeScroll] instead.
+ *
+ * See [androidx.compose.foundation.gestures.detectTransformGestures] for more details.
+ */
+fun Modifier.freeScrollWithTransformGesture(
+ state: FreeScrollState,
+ enabled: Boolean = true,
+ panZoomLock: Boolean = false,
+ horizontalReverseScrolling: Boolean = false,
+ verticalReverseScrolling: Boolean = false,
+ flingBehavior: FlingBehavior? = null,
+ onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit,
+): Modifier = composed {
+
+ val velocityTracker = remember { VelocityTracker() }
+ val fling = flingBehavior ?: ScrollableDefaults.flingBehavior()
+
+ this.horizontalScroll(
+ state = state.horizontalScrollState,
+ enabled = false,
+ reverseScrolling = horizontalReverseScrolling
+ ).verticalScroll(
+ state = state.verticalScrollState,
+ enabled = false,
+ reverseScrolling = verticalReverseScrolling
+ )
+ .pointerInput(enabled, horizontalReverseScrolling, verticalReverseScrolling) {
+ if (!enabled) return@pointerInput
+
+ coroutineScope {
+ detectFreeScrollGestures(
+ panZoomLock = panZoomLock,
+ onGesture = { centroid, pan, zoom, rotation, change ->
+ onDrag(
+ change = change,
+ dragAmount = pan,
+ state = state,
+ horizontalReverseScrolling = horizontalReverseScrolling,
+ verticalReverseScrolling = verticalReverseScrolling,
+ velocityTracker = velocityTracker,
+ coroutineScope = this
+ )
+ onGesture(centroid, pan, zoom, rotation)
+ },
+ onEnd = {
+ onEnd(
+ velocityTracker = velocityTracker,
+ state = state,
+ horizontalReverseScrolling = horizontalReverseScrolling,
+ verticalReverseScrolling = verticalReverseScrolling,
+ flingBehavior = fling,
+ coroutineScope = this
+ )
+ }
+ )
+ }
+ }
+}
+
+
+/**
+ * If [change] is null, it means that the id of the pointer is changed. This happens when
+ * freeScrollWithTransformGesture is used. In this case, we need to reset the velocity tracker to
+ * avoid incorrect velocity calculation.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+private fun onDrag(
+ change: PointerInputChange?,
+ dragAmount: Offset,
+ state: FreeScrollState,
+ horizontalReverseScrolling: Boolean,
+ verticalReverseScrolling: Boolean,
+ velocityTracker: VelocityTracker,
+ coroutineScope: CoroutineScope,
+) {
+
+ coroutineScope.launch {
+ state.horizontalScrollState.scrollBy(
+ if (horizontalReverseScrolling) dragAmount.x else -dragAmount.x
+ )
+ state.verticalScrollState.scrollBy(
+ if (verticalReverseScrolling) dragAmount.y else -dragAmount.y
+ )
+ }
+
+ if (change == null) {
+ velocityTracker.resetTracking()
+ return
+ }
+
+ // Add historical position to velocity tracker to increase accuracy
+ val changeList = change.historical.map {
+ it.uptimeMillis to it.position
+ } + (change.uptimeMillis to change.position)
+
+ changeList.forEach { (time, pos) ->
+ val position = Offset(
+ x = pos.x - if (horizontalReverseScrolling)
+ -state.horizontalScrollState.value
+ else
+ state.horizontalScrollState.value,
+ y = pos.y - if (verticalReverseScrolling)
+ -state.verticalScrollState.value
+ else
+ state.verticalScrollState.value,
+ )
+ velocityTracker.addPosition(time, position)
+ }
+}
+
+
+private fun onEnd(
+ velocityTracker: VelocityTracker,
+ state: FreeScrollState,
+ horizontalReverseScrolling: Boolean,
+ verticalReverseScrolling: Boolean,
+ flingBehavior: FlingBehavior,
+ coroutineScope: CoroutineScope
+) {
+ val velocity = velocityTracker.calculateVelocity()
+ velocityTracker.resetTracking()
+
+ // Launch two animation separately to make sure they work simultaneously.
+ coroutineScope.launch {
+ state.horizontalScrollState.scroll {
+ with(flingBehavior) {
+ performFling(if (horizontalReverseScrolling) velocity.x else -velocity.x)
+ }
+ }
+ }
+ coroutineScope.launch {
+ state.verticalScrollState.scroll {
+ with(flingBehavior) {
+ performFling(if (verticalReverseScrolling) velocity.y else -velocity.y)
+ }
+ }
+ }
+}
+
+internal suspend fun PointerInputScope.detectFreeScrollGestures(
+ panZoomLock: Boolean = false,
+ onGesture: (
+ centroid: Offset,
+ pan: Offset,
+ zoom: Float,
+ rotation: Float,
+ change: PointerInputChange?
+ ) -> Unit,
+ onEnd: () -> Unit = {}
+) {
+ awaitEachGesture {
+ var rotation = 0f
+ var zoom = 1f
+ var pan = Offset.Zero
+ var pastTouchSlop = false
+ val touchSlop = viewConfiguration.touchSlop
+ var lockedToPanZoom = false
+
+ val down: PointerInputChange = awaitFirstDown(requireUnconsumed = false)
+
+ // Drag event
+ val pointer: PointerId = down.id
+
+ do {
+ val event = awaitPointerEvent()
+ val canceled = event.changes.fastAny { it.isConsumed }
+
+ if (!canceled) {
+ val dragEvent = event.changes.fastFirstOrNull { it.id == pointer }
+
+ val zoomChange = event.calculateZoom()
+ val rotationChange = event.calculateRotation()
+ val panChange = event.calculatePan()
+
+ if (!pastTouchSlop) {
+ zoom *= zoomChange
+ rotation += rotationChange
+ pan += panChange
+
+ val centroidSize = event.calculateCentroidSize(useCurrent = false)
+ val zoomMotion = abs(1 - zoom) * centroidSize
+ val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f)
+ val panMotion = pan.getDistance()
+
+ if (zoomMotion > touchSlop ||
+ rotationMotion > touchSlop ||
+ panMotion > touchSlop
+ ) {
+ pastTouchSlop = true
+ lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
+ }
+ }
+
+ if (pastTouchSlop) {
+ val centroid = event.calculateCentroid(useCurrent = false)
+ val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
+ if (effectiveRotation != 0f ||
+ zoomChange != 1f ||
+ panChange != Offset.Zero
+ ) {
+ onGesture(centroid, panChange, zoomChange, effectiveRotation, dragEvent)
+ }
+ event.changes.fastForEach {
+ if (it.positionChanged()) {
+ it.consume()
+ }
+ }
+ }
+ }
+ } while (!canceled && event.changes.fastAny { it.pressed })
+
+ onEnd()
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/ui/utils/modifier/model/FreeScrollState.kt b/app/src/main/java/com/a404m/mine_game/ui/utils/modifier/model/FreeScrollState.kt
new file mode 100644
index 0000000..2e55dc0
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/ui/utils/modifier/model/FreeScrollState.kt
@@ -0,0 +1,141 @@
+package com.a404m.mine_game.ui.utils.modifier.model
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.Offset
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+
+class FreeScrollState(
+ val horizontalScrollState: ScrollState,
+ val verticalScrollState: ScrollState,
+) {
+
+ /**
+ * current horizontal scroll position value in pixels
+ */
+ val xValue: Int get() = horizontalScrollState.value
+
+ /**
+ * current vertical scroll position value in pixels
+ */
+ val yValue: Int get() = verticalScrollState.value
+
+
+ /**
+ * maximum bound for [xValue], or [Int.MAX_VALUE] if still unknown
+ */
+ val xMaxValue: Int get() = horizontalScrollState.maxValue
+
+ /**
+ * maximum bound for [yValue], or [Int.MAX_VALUE] if still unknown
+ */
+ val yMaxValue: Int get() = verticalScrollState.maxValue
+
+
+ /**
+ * Jump instantly by [offset] pixels.
+ *
+ * @see animateScrollBy for an animated version
+ *
+ * @param offset number of pixels to scroll by
+ * @return the amount of scroll consumed
+ */
+ suspend fun scrollBy(
+ offset: Offset
+ ): Offset = coroutineScope {
+ val xOffset = async {
+ horizontalScrollState.scrollBy(offset.x)
+ }
+ val yOffset = async {
+ verticalScrollState.scrollBy(offset.y)
+ }
+ Offset(xOffset.await(), yOffset.await())
+ }
+
+ /**
+ * Instantly jump to the given position in pixels.
+ *
+ * @see animateScrollTo for an animated version
+ *
+ * @param x the horizontal position to scroll to
+ * @param y the vertical position to scroll to
+ * @return the amount of scroll consumed
+ */
+ suspend fun scrollTo(
+ x: Int,
+ y: Int,
+ ): Offset = coroutineScope {
+ val xOffset = async {
+ horizontalScrollState.scrollTo(x)
+ }
+ val yOffset = async {
+ verticalScrollState.scrollTo(y)
+ }
+ Offset(xOffset.await(), yOffset.await())
+ }
+
+ /**
+ * Scroll by [offset] pixels with animation.
+ *
+ * @param offset number of pixels to scroll by
+ * @param animationSpec [AnimationSpec] to be used for this scrolling
+ *
+ * @return the amount of scroll consumed
+ */
+ suspend fun animateScrollBy(
+ offset: Offset,
+ animationSpec: AnimationSpec<Float> = spring(),
+ ): Offset = coroutineScope {
+ val xOffset = async {
+ horizontalScrollState.animateScrollBy(offset.x, animationSpec)
+ }
+ val yOffset = async {
+ verticalScrollState.animateScrollBy(offset.y, animationSpec)
+ }
+ Offset(xOffset.await(), yOffset.await())
+ }
+
+ /**
+ * Scroll to the given position in pixels with animation.
+ *
+ * @param x the horizontal position to scroll to
+ * @param y the vertical position to scroll to
+ * @param animationSpec [AnimationSpec] to be used for this scrolling
+ */
+ suspend fun animateScrollTo(
+ x: Int,
+ y: Int,
+ animationSpec: AnimationSpec<Float> = spring(),
+ ) = coroutineScope {
+ val xOffset = async {
+ horizontalScrollState.animateScrollTo(x, animationSpec)
+ }
+ val yOffset = async {
+ verticalScrollState.animateScrollTo(y, animationSpec)
+ }
+ awaitAll(xOffset, yOffset)
+ }
+}
+
+/**
+ * Create and [remember] [FreeScrollState] that is used to control and observe scrolling
+ *
+ * @param initialX initial horizontal scroller position
+ * @param initialY initial vertical scroller position
+ */
+@Composable
+fun rememberFreeScrollState(initialX: Int = 0, initialY: Int = 0): FreeScrollState {
+ val horizontalScrollState = rememberScrollState(initialX)
+ val verticalScrollState = rememberScrollState(initialY)
+ return remember {
+ FreeScrollState(horizontalScrollState, verticalScrollState)
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/utils/Application.kt b/app/src/main/java/com/a404m/mine_game/utils/Application.kt
new file mode 100644
index 0000000..72dd242
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/utils/Application.kt
@@ -0,0 +1,14 @@
+package com.a404m.mine_game.utils
+
+import android.content.Context
+
+fun getApplicationName(context: Context): String {
+ val applicationInfo = context.applicationInfo
+ val stringId = applicationInfo.labelRes
+
+ return if (stringId == 0) {
+ applicationInfo.nonLocalizedLabel.toString()
+ } else {
+ context.getString(stringId)
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/utils/Extensions.kt b/app/src/main/java/com/a404m/mine_game/utils/Extensions.kt
new file mode 100644
index 0000000..92ef621
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/utils/Extensions.kt
@@ -0,0 +1,85 @@
+package com.a404m.mine_game.utils
+
+import androidx.compose.ui.graphics.Color
+import org.json.JSONArray
+import org.json.JSONObject
+
+fun String.toPersianNumbers(): String {
+ val result = StringBuilder()
+
+ for (c in this) {
+ result.append(
+ when (c) {
+ '0' -> '۰'
+ '1' -> '۱'
+ '2' -> '۲'
+ '3' -> '۳'
+ '4' -> '۴'
+ '5' -> '۵'
+ '6' -> '۶'
+ '7' -> '۷'
+ '8' -> '۸'
+ '9' -> '۹'
+ else -> c
+ }
+ )
+ }
+
+ return result.toString()
+}
+
+fun Color.lerp(
+ color: Color,
+ t: Float,
+): Color {
+ return this.copy(
+ alpha = this.alpha * (1 - t) + color.alpha * t,
+ red = this.red * (1 - t) + color.red * t,
+ blue = this.blue * (1 - t) + color.blue * t,
+ green = this.green * (1 - t) + color.green * t,
+ )
+}
+
+fun <T> JSONArray.map(transform: (Any) -> T): ArrayList<T> {
+ val result = arrayListOf<T>()
+
+ for (i in 0 until this.length()) {
+ result.add(transform(this.get(i)))
+ }
+
+ return result
+}
+
+fun JSONObject.getDoubleOrNull(key: String): Double? {
+ return if (this.isNull(key)) {
+ null
+ } else {
+ this.getDouble(key)
+ }
+}
+
+fun JSONObject.getIntOrNull(key: String): Int? {
+ return if (this.isNull(key)) {
+ null
+ } else {
+ this.getInt(key)
+ }
+}
+
+fun JSONObject.getStringOrNull(key: String): String? {
+ return if (this.isNull(key)) {
+ null
+ } else {
+ this.getString(key)
+ }
+}
+
+fun <T> List<T>.toJson(): JSONArray {
+ val result = JSONArray()
+
+ for (item in this) {
+ result.put(item)
+ }
+
+ return result
+} \ No newline at end of file
diff --git a/app/src/main/java/com/a404m/mine_game/utils/PersianDate.kt b/app/src/main/java/com/a404m/mine_game/utils/PersianDate.kt
new file mode 100644
index 0000000..9352348
--- /dev/null
+++ b/app/src/main/java/com/a404m/mine_game/utils/PersianDate.kt
@@ -0,0 +1,539 @@
+package com.a404m.mine_game.utils
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import java.util.Date
+import kotlin.math.min
+import kotlin.reflect.KProperty
+
+
+class PersianDate(private var millisecondsSinceEpoch: Long) : Comparable<PersianDate> {
+ companion object {
+ const val MILLISECONDS: Long = 1000
+ const val SECONDS = 60 * MILLISECONDS
+ const val MINUTES = 60 * SECONDS
+ const val HOURS = 24 * MINUTES
+ private const val FOUR_YEARS_DAYS: Long = 4 * 365 + 1
+ private const val FOUR_YEARS_MILLISECONDS = FOUR_YEARS_DAYS * HOURS
+
+ const val FIRST_YEAR: Int = 1348 // 1348/10/11
+ private const val FIRST_DATE_DAYS = 286
+ private const val TO_YEAR_OFFSET = 365 - FIRST_DATE_DAYS
+
+ private const val FIRST_WEEK_DAY_OF_TIME = 5
+
+ private const val LEAP_YEAR = 3
+
+ val MONTH_NAMES = arrayOf(
+ "فروردین",
+ "اردیبهشت",
+ "خرداد",
+ "تیر",
+ "مرداد",
+ "شهریور",
+ "مهر",
+ "آبان",
+ "آذر",
+ "دی",
+ "بهمن",
+ "اسفند",
+ )
+
+ val WEEK_DAY_NAMES = arrayOf(
+ "شنبه",
+ "یک شنبه",
+ "دو شنبه",
+ "سه شنبه",
+ "چهار شنبه",
+ "پنج شنبه",
+ "جمعه",
+ )
+
+ @JvmStatic
+ fun today(): PersianDate {
+ val persianDate = PersianDate()
+ persianDate.startOfDay()
+ return persianDate
+ }
+
+ @JvmStatic
+ fun tomorrow(): PersianDate {
+ val persianDate = PersianDate()
+ persianDate.startOfDay()
+ return persianDate
+ }
+
+ @JvmStatic
+ fun init(
+ year: Int,
+ month: Int,
+ day: Int,
+ hour: Int = 0,
+ minute: Int = 0,
+ second: Int = 0,
+ millisecond: Int = 0
+ ): PersianDate {
+ if (year < FIRST_YEAR + 1)
+ throw RuntimeException("bad year")
+ if (month !in 0 until 12)
+ throw RuntimeException("bad month")
+ else if (day !in 0 until getMonthDays(
+ year,
+ month
+ )
+ )
+ throw RuntimeException("bad day")
+ else if (hour !in 0 until 24)
+ throw RuntimeException("bad hour")
+ else if (minute !in 0 until 60)
+ throw RuntimeException("bad minute")
+ else if (second !in 0 until 60)
+ throw RuntimeException("bad second")
+ else if (millisecond !in 0 until 1000)
+ throw RuntimeException("bad millisecond")
+
+
+ var millisecondsSinceEpoch: Long = TO_YEAR_OFFSET * HOURS +
+ millisecond +
+ second * MILLISECONDS +
+ minute * SECONDS +
+ hour * MINUTES +
+ day * HOURS +
+ getYearDaysUntilMonth(
+ year,
+ month
+ ) * HOURS
+
+ val calculatingYear = year - FIRST_YEAR - 1
+
+ millisecondsSinceEpoch += (calculatingYear / 4) * FOUR_YEARS_MILLISECONDS
+
+ var yearToCalc = year - (calculatingYear % 4)
+
+ while (yearToCalc < year) {
+ millisecondsSinceEpoch += (if (isLeap(yearToCalc)) 366 else 365) * HOURS
+ ++yearToCalc
+ }
+
+ return PersianDate(millisecondsSinceEpoch)
+ }
+
+ private fun isLeap(year: Int): Boolean {
+ return year % 4 == LEAP_YEAR
+ }
+
+ private fun getMonthDays(
+ year: Int,
+ month: Int
+ ): Int {
+ return if (month < 6) {
+ 31
+ } else if (month != 11 || isLeap(year)) {
+ 30
+ } else {
+ 29
+ }
+ }
+
+ private fun getYearDaysUntilMonth(
+ year: Int,
+ month: Int
+ ): Int {
+ return if (month <= 6)
+ month * 31
+ else if (month == 12)
+ 365 + (if (isLeap(year)) 1 else 0)
+ else
+ 6 * 31 + (month - 6) * 30
+ }
+
+ fun parse(str: String): PersianDate? {
+ val sep = str.split('/')
+ if (sep.size != 3 || sep.any { it.toIntOrNull() == null }) {
+ return null
+ }
+
+ val date = PersianDate()
+ date.setYear(sep[0].toInt())
+ date.setMonth(sep[1].toInt() - 1)
+ date.setDay(sep[2].toInt() - 1)
+
+ return date
+ }
+ }
+
+ // todo: make a better way
+ constructor() : this(Date().time + (3 * MINUTES + 30 * SECONDS))
+
+ constructor(date: Date) : this(date.time)
+
+ constructor(persianDate: PersianDate) : this(persianDate.millisecondsSinceEpoch)
+
+ fun getMillisecondsSinceEpoch() = millisecondsSinceEpoch
+
+ fun getMillisecond(): Int {
+ return (millisecondsSinceEpoch % 1000).toInt()
+ }
+
+ fun getSecond(): Int {
+ return ((millisecondsSinceEpoch / MILLISECONDS) % 60).toInt()
+ }
+
+ fun getMinute(): Int {
+ return ((millisecondsSinceEpoch / SECONDS) % 60).toInt()
+ }
+
+ fun getHour(): Int {
+ return ((millisecondsSinceEpoch / MINUTES) % 24).toInt()
+ }
+
+ fun getDays(): Long {
+ return millisecondsSinceEpoch / HOURS
+ }
+
+ fun getYearDay(): Int {
+ var days = getDays().toInt()
+ if (days < TO_YEAR_OFFSET)
+ return FIRST_DATE_DAYS + days
+ days -= TO_YEAR_OFFSET
+
+ days %= FOUR_YEARS_DAYS.toInt()
+
+ return if (days < 366)
+ days
+ else if (days < 365 * 3)
+ days % 365
+ else if (days == 365 * 3)
+ 365
+ else
+ (days - 366) % 365
+ }
+
+ fun getDay(): Int {
+ val days = getYearDay()
+ return if (days < 31 * 6)
+ days % 31
+ else
+ (days - 31 * 6) % 30
+ }
+
+ fun getHumanDay(): Int {
+ return getDay() + 1
+ }
+
+ fun getMonth(): Int {
+ val days = getYearDay()
+ return if (days < 31 * 6) {
+ days / 31
+ } else {
+ (days - 31 * 6) / 30 + 6
+ }
+ }
+
+ fun getHumanMonth(): Int {
+ return getMonth() + 1
+ }
+
+ fun getYear(): Int {
+ var days = getDays()
+ var year = FIRST_YEAR + (days / FOUR_YEARS_DAYS) * 4
+ days %= FOUR_YEARS_DAYS
+ days -= TO_YEAR_OFFSET
+ if (days < 0) {
+ return year.toInt()
+ }
+ ++year
+ days -= 366
+ if (days < 0) {
+ return year.toInt()
+ }
+ year += 1 + days / 365
+ return year.toInt()
+ }
+
+ fun getWeekDay(): Int {
+ return ((getDays() + FIRST_WEEK_DAY_OF_TIME) % 7).toInt()
+ }
+
+ fun getWeekDayString(): String {
+ return WEEK_DAY_NAMES[getWeekDay()]
+ }
+
+ fun getMonthDays(): Int {
+ return getMonthDays(
+ getYear(),
+ getMonth()
+ )
+ }
+
+ fun getTime(): Long {
+ return millisecondsSinceEpoch
+ }
+
+ fun setMillisecond(milliseconds: Int) {
+ if (milliseconds !in 0 until 1000)
+ throw RuntimeException("bad milliseconds $milliseconds")
+
+ this.millisecondsSinceEpoch -= millisecondsSinceEpoch % MILLISECONDS
+ this.millisecondsSinceEpoch += milliseconds
+ }
+
+ fun setSecond(second: Int) {
+ if (second !in 0 until 60)
+ throw RuntimeException("bad second $second")
+
+ val diff = second - getSecond()
+ this.millisecondsSinceEpoch += diff * MILLISECONDS
+ }
+
+ fun setMinute(minute: Int) {
+ if (minute !in 0 until 60)
+ throw RuntimeException("bad minute $minute")
+
+ val diff = minute - getMinute()
+ this.millisecondsSinceEpoch += diff * SECONDS
+ }
+
+ fun setHour(hour: Int) {
+ if (hour !in 0 until 60)
+ throw RuntimeException("bad hour $hour")
+
+ val diff = hour - getHour()
+ this.millisecondsSinceEpoch += diff * MINUTES
+ }
+
+ fun setDay(day: Int) {
+ if (day !in 0 until getMonthDays())
+ throw RuntimeException("bad day $day")
+
+ val diff = day - getDay()
+ this.millisecondsSinceEpoch += diff * HOURS
+ }
+
+ fun setMonth(month: Int) {
+ if (month !in 0 until 12)
+ throw RuntimeException("bad month $month")
+
+ val year = getYear()
+ val newMonthDays = getMonthDays(
+ year,
+ month
+ )
+
+ val day = min(
+ getDay(),
+ newMonthDays - 1
+ )
+
+ val dayDiff = day + getYearDaysUntilMonth(
+ year,
+ month
+ ) - getYearDay()
+
+ this.millisecondsSinceEpoch += dayDiff * HOURS
+ }
+
+ fun setYear(year: Int) {
+ if (year < FIRST_YEAR + 1)
+ throw RuntimeException("bad year $year")
+
+ val currentYear = getYear()
+ addYears(year - currentYear)
+ }
+
+ fun isLeap(): Boolean {
+ return isLeap(getYear())
+ }
+
+ fun addMilliseconds(milliseconds: Long) {
+ millisecondsSinceEpoch += milliseconds
+ }
+
+ fun addSeconds(seconds: Long) {
+ millisecondsSinceEpoch += seconds * MILLISECONDS
+ }
+
+ fun addMinutes(minutes: Long) {
+ millisecondsSinceEpoch += minutes * SECONDS
+ }
+
+ fun addHours(hours: Long) {
+ millisecondsSinceEpoch += hours * MINUTES
+ }
+
+ fun addDays(days: Long) {
+ millisecondsSinceEpoch += days * HOURS
+ }
+
+ fun addMonths(months: Int) {
+ addYears(months / 12)
+ var monthsLeft = months % 12
+
+ var year = getYear()
+ var month = getMonth()
+ while (monthsLeft > 0) {
+ addDays(
+ getMonthDays(
+ year,
+ month
+ ).toLong()
+ )
+
+ --monthsLeft
+ month = (month + 1) % 12
+ if (month == 0) {
+ ++year
+ }
+ }
+ }
+
+ fun addYears(years: Int) {
+ var currentYear = getYear()
+ val year = years + currentYear
+
+ val isLeapDay = getMonth() == 11 && getDay() == 29
+
+ var yearDiff = year - currentYear
+ val dayDiff = if (isLeapDay && !isLeap(year)) 1 else 0
+
+ var milliseconds = (yearDiff / 4) * FOUR_YEARS_MILLISECONDS - dayDiff * HOURS
+ currentYear += yearDiff
+ yearDiff %= 4
+
+ while (yearDiff > 0) {
+ milliseconds += (if (isLeap(currentYear - 1)) 366 else 365) * HOURS
+ ++currentYear
+ --yearDiff
+ }
+
+ while (yearDiff < 0) {
+ milliseconds -= (if (isLeap(currentYear)) 366 else 365) * HOURS
+ --currentYear
+ ++yearDiff
+ }
+
+ this.millisecondsSinceEpoch += milliseconds
+ }
+
+ fun addMillisecond() {
+ this.addMilliseconds(1)
+ }
+
+ fun addSecond() {
+ this.addSeconds(1)
+ }
+
+ fun addMinute() {
+ this.addMinutes(1)
+ }
+
+ fun addHour() {
+ this.addHours(1)
+ }
+
+ fun addDay() {
+ this.addDays(1)
+ }
+
+ fun addMonth() {
+ this.addMonths(1)
+ }
+
+ fun addYear() {
+ this.addYears(1)
+ }
+
+ fun startOfDay() {
+ millisecondsSinceEpoch -= millisecondsSinceEpoch % HOURS
+ }
+
+ fun endOfDay() {
+ this.startOfDay()
+ millisecondsSinceEpoch += 23 * MINUTES + 59 * SECONDS + 59 * MILLISECONDS + 999
+ }
+
+ override fun compareTo(other: PersianDate): Int {
+ return this.millisecondsSinceEpoch.compareTo(other.millisecondsSinceEpoch)
+ }
+
+ fun isInPastDays(): Boolean {
+ return this < today()
+ }
+
+ fun isInFutureDays(): Boolean {
+ return this >= tomorrow()
+ }
+
+ fun getMonthName(): String {
+ return MONTH_NAMES[getMonth()]
+ }
+
+ override fun toString(): String {
+ return "${this.getYear()}/${this.getHumanMonth()}/${this.getHumanDay()} ${this.getHour()}:${this.getMinute()}:${this.getSecond()}.${this.getMillisecond()}"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return when (other) {
+ is PersianDate -> other.millisecondsSinceEpoch == this.millisecondsSinceEpoch
+ else -> false
+ }
+ }
+
+ override fun hashCode(): Int {
+ return millisecondsSinceEpoch.hashCode()
+ }
+
+ operator fun getValue(
+ nothing: Nothing?,
+ property: KProperty<*>
+ ): PersianDate {
+ return this
+ }
+
+ operator fun setValue(
+ nothing: Nothing?,
+ property: KProperty<*>,
+ date: PersianDate
+ ) {
+ this.millisecondsSinceEpoch = date.millisecondsSinceEpoch
+ }
+
+ fun toRelativeString(): String {
+ val now = PersianDate()
+ val diff = now.millisecondsSinceEpoch - this.millisecondsSinceEpoch
+ return if (diff < MILLISECONDS) {
+ "الان"
+ } else if (diff < SECONDS) {
+ "${diff / MILLISECONDS} ثانیه قبل"
+ } else if (diff < MINUTES) {
+ "${diff / SECONDS} دقیقه قبل"
+ } else if (diff < HOURS) {
+ "${diff / MINUTES} ساعت قبل"
+ } else {
+ "${diff / HOURS} روز قبل"
+ }
+ }
+
+ fun toYMD(): String = "${getYear()}/${getMonth()}/${getDay()}"
+
+ fun toHM(): String = "${
+ getHour()
+ }:${
+ getMinute().toString().padStart(
+ 2,
+ '0'
+ )
+ }"
+}
+
+val PersianDateSaver = Saver<PersianDate, Long>(
+ save = { it.getTime() },
+ restore = { PersianDate(it) },
+)
+
+@Composable
+fun rememberSavablePersianDate(init: () -> PersianDate) = rememberSaveable(
+ saver = PersianDateSaver,
+ init = init
+) \ No newline at end of file
diff --git a/app/src/main/res/drawable/about_us.xml b/app/src/main/res/drawable/about_us.xml
new file mode 100644
index 0000000..3218106
--- /dev/null
+++ b/app/src/main/res/drawable/about_us.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,17c-0.55,0 -1,-0.45 -1,-1v-4c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v4c0,0.55 -0.45,1 -1,1zM13,9h-2L11,7h2v2z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/add.xml b/app/src/main/res/drawable/add.xml
new file mode 100644
index 0000000..0c707a7
--- /dev/null
+++ b/app/src/main/res/drawable/add.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/arrow_back.xml b/app/src/main/res/drawable/arrow_back.xml
new file mode 100644
index 0000000..85258df
--- /dev/null
+++ b/app/src/main/res/drawable/arrow_back.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M19,11H7.83l4.88,-4.88c0.39,-0.39 0.39,-1.03 0,-1.42 -0.39,-0.39 -1.02,-0.39 -1.41,0l-6.59,6.59c-0.39,0.39 -0.39,1.02 0,1.41l6.59,6.59c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L7.83,13H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/arrow_drop_down.xml b/app/src/main/res/drawable/arrow_drop_down.xml
new file mode 100644
index 0000000..d1e8d53
--- /dev/null
+++ b/app/src/main/res/drawable/arrow_drop_down.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M8.71,11.71l2.59,2.59c0.39,0.39 1.02,0.39 1.41,0l2.59,-2.59c0.63,-0.63 0.18,-1.71 -0.71,-1.71H9.41c-0.89,0 -1.33,1.08 -0.7,1.71z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/arrow_drop_up.xml b/app/src/main/res/drawable/arrow_drop_up.xml
new file mode 100644
index 0000000..119a094
--- /dev/null
+++ b/app/src/main/res/drawable/arrow_drop_up.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M8.71,12.29L11.3,9.7c0.39,-0.39 1.02,-0.39 1.41,0l2.59,2.59c0.63,0.63 0.18,1.71 -0.71,1.71H9.41c-0.89,0 -1.33,-1.08 -0.7,-1.71z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/controls.xml b/app/src/main/res/drawable/controls.xml
new file mode 100644
index 0000000..2d249fc
--- /dev/null
+++ b/app/src/main/res/drawable/controls.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M15,7.29L15,3c0,-0.55 -0.45,-1 -1,-1h-4c-0.55,0 -1,0.45 -1,1v4.29c0,0.13 0.05,0.26 0.15,0.35l2.5,2.5c0.2,0.2 0.51,0.2 0.71,0l2.5,-2.5c0.09,-0.09 0.14,-0.21 0.14,-0.35zM7.29,9L3,9c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h4.29c0.13,0 0.26,-0.05 0.35,-0.15l2.5,-2.5c0.2,-0.2 0.2,-0.51 0,-0.71l-2.5,-2.5C7.55,9.05 7.43,9 7.29,9zM9,16.71L9,21c0,0.55 0.45,1 1,1h4c0.55,0 1,-0.45 1,-1v-4.29c0,-0.13 -0.05,-0.26 -0.15,-0.35l-2.5,-2.5c-0.2,-0.2 -0.51,-0.2 -0.71,0l-2.5,2.5c-0.09,0.09 -0.14,0.21 -0.14,0.35zM16.35,9.15l-2.5,2.5c-0.2,0.2 -0.2,0.51 0,0.71l2.5,2.5c0.09,0.09 0.22,0.15 0.35,0.15L21,15.01c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1h-4.29c-0.14,-0.01 -0.26,0.04 -0.36,0.14z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml
new file mode 100644
index 0000000..2a144e2
--- /dev/null
+++ b/app/src/main/res/drawable/delete.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V9c0,-1.1 -0.9,-2 -2,-2H8c-1.1,0 -2,0.9 -2,2v10zM18,4h-2.5l-0.71,-0.71c-0.18,-0.18 -0.44,-0.29 -0.7,-0.29H9.91c-0.26,0 -0.52,0.11 -0.7,0.29L8.5,4H6c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h12c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/donate.xml b/app/src/main/res/drawable/donate.xml
new file mode 100644
index 0000000..6a0360c
--- /dev/null
+++ b/app/src/main/res/drawable/donate.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M11.8,10.9c-2.27,-0.59 -3,-1.2 -3,-2.15 0,-1.09 1.01,-1.85 2.7,-1.85 1.42,0 2.13,0.54 2.39,1.4 0.12,0.4 0.45,0.7 0.87,0.7h0.3c0.66,0 1.13,-0.65 0.9,-1.27 -0.42,-1.18 -1.4,-2.16 -2.96,-2.54V4.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,3.67 10,4.5v0.66c-1.94,0.42 -3.5,1.68 -3.5,3.61 0,2.31 1.91,3.46 4.7,4.13 2.5,0.6 3,1.48 3,2.41 0,0.69 -0.49,1.79 -2.7,1.79 -1.65,0 -2.5,-0.59 -2.83,-1.43 -0.15,-0.39 -0.49,-0.67 -0.9,-0.67h-0.28c-0.67,0 -1.14,0.68 -0.89,1.3 0.57,1.39 1.9,2.21 3.4,2.53v0.67c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-0.65c1.95,-0.37 3.5,-1.5 3.5,-3.55 0,-2.84 -2.43,-3.81 -4.7,-4.4z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/flag.xml b/app/src/main/res/drawable/flag.xml
new file mode 100644
index 0000000..5e0ddfb
--- /dev/null
+++ b/app/src/main/res/drawable/flag.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M14.4,6l-0.24,-1.2c-0.09,-0.46 -0.5,-0.8 -0.98,-0.8H6c-0.55,0 -1,0.45 -1,1v15c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-6h5.6l0.24,1.2c0.09,0.47 0.5,0.8 0.98,0.8H19c0.55,0 1,-0.45 1,-1V7c0,-0.55 -0.45,-1 -1,-1h-4.6z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/hint.xml b/app/src/main/res/drawable/hint.xml
new file mode 100644
index 0000000..d49a6ff
--- /dev/null
+++ b/app/src/main/res/drawable/hint.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M400,720Q367,720 343.5,696.5Q320,673 320,640L320,590Q263,551 231.5,490Q200,429 200,360Q200,243 281.5,161.5Q363,80 480,80Q597,80 678.5,161.5Q760,243 760,360Q760,429 728.5,489.5Q697,550 640,590L640,640Q640,673 616.5,696.5Q593,720 560,720L400,720ZM400,640L560,640Q560,640 560,640Q560,640 560,640L560,569Q560,559 564.5,550Q569,541 577,536L594,524Q635,496 657.5,452.5Q680,409 680,360Q680,277 621.5,218.5Q563,160 480,160Q397,160 338.5,218.5Q280,277 280,360Q280,409 302.5,452.5Q325,496 366,524L383,536Q391,541 395.5,550Q400,559 400,569L400,640Q400,640 400,640Q400,640 400,640ZM400,880Q383,880 371.5,868.5Q360,857 360,840Q360,823 371.5,811.5Q383,800 400,800L560,800Q577,800 588.5,811.5Q600,823 600,840Q600,857 588.5,868.5Q577,880 560,880L400,880ZM480,360Q480,360 480,360Q480,360 480,360L480,360Q480,360 480,360Q480,360 480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360L480,360Q480,360 480,360Q480,360 480,360L480,360Q480,360 480,360Q480,360 480,360Z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/history.xml b/app/src/main/res/drawable/history.xml
new file mode 100644
index 0000000..f7879b4
--- /dev/null
+++ b/app/src/main/res/drawable/history.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M13.26,3C8.17,2.86 4,6.95 4,12L2.21,12c-0.45,0 -0.67,0.54 -0.35,0.85l2.79,2.8c0.2,0.2 0.51,0.2 0.71,0l2.79,-2.8c0.31,-0.31 0.09,-0.85 -0.36,-0.85L6,12c0,-3.9 3.18,-7.05 7.1,-7 3.72,0.05 6.85,3.18 6.9,6.9 0.05,3.91 -3.1,7.1 -7,7.1 -1.61,0 -3.1,-0.55 -4.28,-1.48 -0.4,-0.31 -0.96,-0.28 -1.32,0.08 -0.42,0.42 -0.39,1.13 0.08,1.49C9,20.29 10.91,21 13,21c5.05,0 9.14,-4.17 9,-9.26 -0.13,-4.69 -4.05,-8.61 -8.74,-8.74zM12.75,8c-0.41,0 -0.75,0.34 -0.75,0.75v3.68c0,0.35 0.19,0.68 0.49,0.86l3.12,1.85c0.36,0.21 0.82,0.09 1.03,-0.26 0.21,-0.36 0.09,-0.82 -0.26,-1.03l-2.88,-1.71v-3.4c0,-0.4 -0.34,-0.74 -0.75,-0.74z"/>
+
+</vector>
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..edecdbf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,21 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="191.71"
+ android:viewportHeight="191.73">
+ <group android:scaleX="0.4582855"
+ android:scaleY="0.45833334"
+ android:translateX="51.92604"
+ android:translateY="51.926876">
+ <path
+ android:pathData="M28.15,164.95C12.91,150.27 41.56,148.63 31.73,129.9 21.68,110.76 4.14,133.33 0.38,112.05 -3.3,91.23 20.85,106.71 23.93,85.79 27.08,64.41 -0.41,72.39 9.08,52.97c9.29,-19 19.71,7.68 34.52,-7.44 15.14,-15.45 -11.8,-25.12 7.32,-35.26 18.71,-9.93 11.42,17.77 32.31,14.23 21.35,-3.62 5.25,-27.24 26.7,-24.23 20.99,2.94 -1.23,21.07 17.76,30.46 19.4,9.59 20.29,-18.96 35.87,-3.95 15.25,14.69 -13.41,16.33 -3.58,35.06 10.05,19.14 27.59,-3.43 31.35,17.84 3.68,20.82 -20.47,5.34 -23.55,26.26 -3.15,21.38 24.34,13.4 14.85,32.82 -9.29,19 -19.71,-7.68 -34.52,7.44 -15.14,15.45 11.8,25.12 -7.32,35.26 -18.71,9.93 -11.42,-17.77 -32.31,-14.23 -21.35,3.62 -5.25,27.24 -26.7,24.23 -20.99,-2.94 1.23,-21.07 -17.76,-30.46 -19.4,-9.59 -20.29,18.96 -35.87,3.95z"
+ android:strokeWidth="0"
+ android:fillColor="#00000000"
+ android:strokeColor="#000000"/>
+ <path
+ android:pathData="M28.15,164.95C12.91,150.27 41.56,148.63 31.73,129.9 21.68,110.76 4.14,133.33 0.38,112.05 -3.3,91.23 20.85,106.71 23.93,85.79 27.08,64.41 -0.41,72.39 9.08,52.97c9.29,-19 19.71,7.68 34.52,-7.44 15.14,-15.45 -11.8,-25.12 7.32,-35.26 18.71,-9.93 11.42,17.77 32.31,14.23 21.35,-3.62 5.25,-27.24 26.7,-24.23 20.99,2.94 -1.23,21.07 17.76,30.46 19.4,9.59 20.29,-18.96 35.87,-3.95 15.25,14.69 -13.41,16.33 -3.58,35.06 10.05,19.14 27.59,-3.43 31.35,17.84 3.68,20.82 -20.47,5.34 -23.55,26.26 -3.15,21.38 24.34,13.4 14.85,32.82 -9.29,19 -19.71,-7.68 -34.52,7.44 -15.14,15.45 11.8,25.12 -7.32,35.26 -18.71,9.93 -11.42,-17.77 -32.31,-14.23 -21.35,3.62 -5.25,27.24 -26.7,24.23 -20.99,-2.94 1.23,-21.07 -17.76,-30.46 -19.4,-9.59 -20.29,18.96 -35.87,3.95z"
+ android:strokeWidth="0"
+ android:fillColor="#303030"
+ android:strokeColor="#00000000"/>
+ </group>
+</vector>
diff --git a/app/src/main/res/drawable/language.xml b/app/src/main/res/drawable/language.xml
new file mode 100644
index 0000000..23fbbb0
--- /dev/null
+++ b/app/src/main/res/drawable/language.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M12.65,15.67c0.14,-0.36 0.05,-0.77 -0.23,-1.05l-2.09,-2.06 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53h1.94c0.54,0 0.99,-0.45 0.99,-0.99v-0.02c0,-0.54 -0.45,-0.99 -0.99,-0.99L10,4L10,3c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v1L1.99,4c-0.54,0 -0.99,0.45 -0.99,0.99 0,0.55 0.45,0.99 0.99,0.99h10.18C11.5,7.92 10.44,9.75 9,11.35c-0.81,-0.89 -1.49,-1.86 -2.06,-2.88 -0.16,-0.29 -0.45,-0.47 -0.78,-0.47 -0.69,0 -1.13,0.75 -0.79,1.35 0.63,1.13 1.4,2.21 2.3,3.21L3.3,16.87c-0.4,0.39 -0.4,1.03 0,1.42 0.39,0.39 1.02,0.39 1.42,0L9,14l2.02,2.02c0.51,0.51 1.38,0.32 1.63,-0.35zM17.5,10c-0.6,0 -1.14,0.37 -1.35,0.94l-3.67,9.8c-0.24,0.61 0.22,1.26 0.87,1.26 0.39,0 0.74,-0.24 0.88,-0.61l0.89,-2.39h4.75l0.9,2.39c0.14,0.36 0.49,0.61 0.88,0.61 0.65,0 1.11,-0.65 0.88,-1.26l-3.67,-9.8c-0.22,-0.57 -0.76,-0.94 -1.36,-0.94zM15.88,17l1.62,-4.33L19.12,17h-3.24z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/mine.xml b/app/src/main/res/drawable/mine.xml
new file mode 100644
index 0000000..9c6a985
--- /dev/null
+++ b/app/src/main/res/drawable/mine.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="191.71dp"
+ android:height="191.73dp"
+ android:viewportWidth="191.71"
+ android:viewportHeight="191.73">
+ <path
+ android:pathData="M28.15,164.95C12.91,150.27 41.56,148.63 31.73,129.9 21.68,110.76 4.14,133.33 0.38,112.05 -3.3,91.23 20.85,106.71 23.93,85.79 27.08,64.41 -0.41,72.39 9.08,52.97c9.29,-19 19.71,7.68 34.52,-7.44 15.14,-15.45 -11.8,-25.12 7.32,-35.26 18.71,-9.93 11.42,17.77 32.31,14.23 21.35,-3.62 5.25,-27.24 26.7,-24.23 20.99,2.94 -1.23,21.07 17.76,30.46 19.4,9.59 20.29,-18.96 35.87,-3.95 15.25,14.69 -13.41,16.33 -3.58,35.06 10.05,19.14 27.59,-3.43 31.35,17.84 3.68,20.82 -20.47,5.34 -23.55,26.26 -3.15,21.38 24.34,13.4 14.85,32.82 -9.29,19 -19.71,-7.68 -34.52,7.44 -15.14,15.45 11.8,25.12 -7.32,35.26 -18.71,9.93 -11.42,-17.77 -32.31,-14.23 -21.35,3.62 -5.25,27.24 -26.7,24.23 -20.99,-2.94 1.23,-21.07 -17.76,-30.46 -19.4,-9.59 -20.29,18.96 -35.87,3.95z"
+ android:strokeWidth="0"
+ android:fillColor="#00000000"
+ android:strokeColor="#000000"/>
+ <path
+ android:pathData="M28.15,164.95C12.91,150.27 41.56,148.63 31.73,129.9 21.68,110.76 4.14,133.33 0.38,112.05 -3.3,91.23 20.85,106.71 23.93,85.79 27.08,64.41 -0.41,72.39 9.08,52.97c9.29,-19 19.71,7.68 34.52,-7.44 15.14,-15.45 -11.8,-25.12 7.32,-35.26 18.71,-9.93 11.42,17.77 32.31,14.23 21.35,-3.62 5.25,-27.24 26.7,-24.23 20.99,2.94 -1.23,21.07 17.76,30.46 19.4,9.59 20.29,-18.96 35.87,-3.95 15.25,14.69 -13.41,16.33 -3.58,35.06 10.05,19.14 27.59,-3.43 31.35,17.84 3.68,20.82 -20.47,5.34 -23.55,26.26 -3.15,21.38 24.34,13.4 14.85,32.82 -9.29,19 -19.71,-7.68 -34.52,7.44 -15.14,15.45 11.8,25.12 -7.32,35.26 -18.71,9.93 -11.42,-17.77 -32.31,-14.23 -21.35,3.62 -5.25,27.24 -26.7,24.23 -20.99,-2.94 1.23,-21.07 -17.76,-30.46 -19.4,-9.59 -20.29,18.96 -35.87,3.95z"
+ android:strokeWidth="0"
+ android:fillColor="#303030"
+ android:strokeColor="#00000000"/>
+</vector>
diff --git a/app/src/main/res/drawable/open.xml b/app/src/main/res/drawable/open.xml
new file mode 100644
index 0000000..8d43bad
--- /dev/null
+++ b/app/src/main/res/drawable/open.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M5.2,15.43c0,-0.65 0.6,-1.13 1.24,-0.99L10,15.24V4.5C10,3.67 10.67,3 11.5,3S13,3.67 13,4.5v6h0.91c0.31,0 0.62,0.07 0.89,0.21l4.09,2.04c0.77,0.38 1.21,1.22 1.09,2.07l-0.63,4.46C19.21,20.27 18.36,21 17.37,21h-6.16c-0.53,0 -1.29,-0.21 -1.66,-0.59l-4.07,-4.29C5.3,15.94 5.2,15.69 5.2,15.43z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/play.xml b/app/src/main/res/drawable/play.xml
new file mode 100644
index 0000000..42c14a7
--- /dev/null
+++ b/app/src/main/res/drawable/play.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/save.xml b/app/src/main/res/drawable/save.xml
new file mode 100644
index 0000000..125b090
--- /dev/null
+++ b/app/src/main/res/drawable/save.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M17.59,3.59c-0.38,-0.38 -0.89,-0.59 -1.42,-0.59L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7.83c0,-0.53 -0.21,-1.04 -0.59,-1.41l-2.82,-2.83zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM13,9L7,9c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2h6c1.1,0 2,0.9 2,2s-0.9,2 -2,2z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml
new file mode 100644
index 0000000..bc83407
--- /dev/null
+++ b/app/src/main/res/drawable/settings.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M19.5,12c0,-0.23 -0.01,-0.45 -0.03,-0.68l1.86,-1.41c0.4,-0.3 0.51,-0.86 0.26,-1.3l-1.87,-3.23c-0.25,-0.44 -0.79,-0.62 -1.25,-0.42l-2.15,0.91c-0.37,-0.26 -0.76,-0.49 -1.17,-0.68l-0.29,-2.31C14.8,2.38 14.37,2 13.87,2h-3.73C9.63,2 9.2,2.38 9.14,2.88L8.85,5.19c-0.41,0.19 -0.8,0.42 -1.17,0.68L5.53,4.96c-0.46,-0.2 -1,-0.02 -1.25,0.42L2.41,8.62c-0.25,0.44 -0.14,0.99 0.26,1.3l1.86,1.41C4.51,11.55 4.5,11.77 4.5,12s0.01,0.45 0.03,0.68l-1.86,1.41c-0.4,0.3 -0.51,0.86 -0.26,1.3l1.87,3.23c0.25,0.44 0.79,0.62 1.25,0.42l2.15,-0.91c0.37,0.26 0.76,0.49 1.17,0.68l0.29,2.31C9.2,21.62 9.63,22 10.13,22h3.73c0.5,0 0.93,-0.38 0.99,-0.88l0.29,-2.31c0.41,-0.19 0.8,-0.42 1.17,-0.68l2.15,0.91c0.46,0.2 1,0.02 1.25,-0.42l1.87,-3.23c0.25,-0.44 0.14,-0.99 -0.26,-1.3l-1.86,-1.41C19.49,12.45 19.5,12.23 19.5,12zM12.04,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5s3.5,1.57 3.5,3.5S13.97,15.5 12.04,15.5z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/statistics.xml b/app/src/main/res/drawable/statistics.xml
new file mode 100644
index 0000000..2d2b4f0
--- /dev/null
+++ b/app/src/main/res/drawable/statistics.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M6,20L6,20c1.1,0 2,-0.9 2,-2v-7c0,-1.1 -0.9,-2 -2,-2h0c-1.1,0 -2,0.9 -2,2v7C4,19.1 4.9,20 6,20z"/>
+
+ <path android:fillColor="@android:color/white" android:pathData="M16,15v3c0,1.1 0.9,2 2,2h0c1.1,0 2,-0.9 2,-2v-3c0,-1.1 -0.9,-2 -2,-2h0C16.9,13 16,13.9 16,15z"/>
+
+ <path android:fillColor="@android:color/white" android:pathData="M12,20L12,20c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h0c-1.1,0 -2,0.9 -2,2v12C10,19.1 10.9,20 12,20z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/theme.xml b/app/src/main/res/drawable/theme.xml
new file mode 100644
index 0000000..830a7f1
--- /dev/null
+++ b/app/src/main/res/drawable/theme.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M18,4V3c0,-0.55 -0.45,-1 -1,-1H5c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1V6h1v4h-9c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-9h7c0.55,0 1,-0.45 1,-1V5c0,-0.55 -0.45,-1 -1,-1h-2z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/time.xml b/app/src/main/res/drawable/time.xml
new file mode 100644
index 0000000..c8b32e4
--- /dev/null
+++ b/app/src/main/res/drawable/time.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM11.78,7h-0.06c-0.4,0 -0.72,0.32 -0.72,0.72v4.72c0,0.35 0.18,0.68 0.49,0.86l4.15,2.49c0.34,0.2 0.78,0.1 0.98,-0.24 0.21,-0.34 0.1,-0.79 -0.25,-0.99l-3.87,-2.3L12.5,7.72c0,-0.4 -0.32,-0.72 -0.72,-0.72z"/>
+
+</vector>
diff --git a/app/src/main/res/drawable/tutorial.xml b/app/src/main/res/drawable/tutorial.xml
new file mode 100644
index 0000000..2863c6d
--- /dev/null
+++ b/app/src/main/res/drawable/tutorial.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+ <path android:fillColor="@android:color/white" android:pathData="M5,13.18v4L12,21l7,-3.82v-4L12,17l-7,-3.82zM12,3L1,9l11,6 9,-4.91V17h2V9L12,3z"/>
+
+</vector>
diff --git a/app/src/main/res/font/iranyekan_bold.ttf b/app/src/main/res/font/iranyekan_bold.ttf
new file mode 100644
index 0000000..a68e403
--- /dev/null
+++ b/app/src/main/res/font/iranyekan_bold.ttf
Binary files differ
diff --git a/app/src/main/res/font/iranyekan_light.ttf b/app/src/main/res/font/iranyekan_light.ttf
new file mode 100644
index 0000000..7a89cfa
--- /dev/null
+++ b/app/src/main/res/font/iranyekan_light.ttf
Binary files differ
diff --git a/app/src/main/res/font/iranyekan_medium.ttf b/app/src/main/res/font/iranyekan_medium.ttf
new file mode 100644
index 0000000..551a67f
--- /dev/null
+++ b/app/src/main/res/font/iranyekan_medium.ttf
Binary files differ
diff --git a/app/src/main/res/font/iranyekan_regular.ttf b/app/src/main/res/font/iranyekan_regular.ttf
new file mode 100644
index 0000000..72d5808
--- /dev/null
+++ b/app/src/main/res/font/iranyekan_regular.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn.ttf b/app/src/main/res/font/vazirmatn.ttf
new file mode 100644
index 0000000..b02ceb0
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_black.ttf b/app/src/main/res/font/vazirmatn_black.ttf
new file mode 100644
index 0000000..4b9bd66
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_black.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_bold.ttf b/app/src/main/res/font/vazirmatn_bold.ttf
new file mode 100644
index 0000000..efa9b09
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_bold.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_extrabold.ttf b/app/src/main/res/font/vazirmatn_extrabold.ttf
new file mode 100644
index 0000000..380bd15
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_extrabold.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_extralight.ttf b/app/src/main/res/font/vazirmatn_extralight.ttf
new file mode 100644
index 0000000..b7b947e
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_extralight.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_light.ttf b/app/src/main/res/font/vazirmatn_light.ttf
new file mode 100644
index 0000000..2dfd5c3
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_light.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_medium.ttf b/app/src/main/res/font/vazirmatn_medium.ttf
new file mode 100644
index 0000000..1e08dd5
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_medium.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_regular.ttf b/app/src/main/res/font/vazirmatn_regular.ttf
new file mode 100644
index 0000000..64e4a81
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_regular.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_semibold.ttf b/app/src/main/res/font/vazirmatn_semibold.ttf
new file mode 100644
index 0000000..6b3842a
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_semibold.ttf
Binary files differ
diff --git a/app/src/main/res/font/vazirmatn_thin.ttf b/app/src/main/res/font/vazirmatn_thin.ttf
new file mode 100644
index 0000000..b7a7d23
--- /dev/null
+++ b/app/src/main/res/font/vazirmatn_thin.ttf
Binary files differ
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..7353dbd
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -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> \ 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..7353dbd
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -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> \ 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..60cf3df
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Binary files 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..d6e8483
--- /dev/null
+++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Binary files 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..5ab41d1
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Binary files 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..2b30cdc
--- /dev/null
+++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Binary files 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..0d22a7f
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Binary files 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..aac6620
--- /dev/null
+++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Binary files 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..96b86e8
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Binary files 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..c7a35ba
--- /dev/null
+++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Binary files 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..956ac3c
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Binary files 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..a3f14e8
--- /dev/null
+++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Binary files differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="purple_200">#FFBB86FC</color>
+ <color name="purple_500">#FF6200EE</color>
+ <color name="purple_700">#FF3700B3</color>
+ <color name="teal_200">#FF03DAC5</color>
+ <color name="teal_700">#FF018786</color>
+ <color name="black">#FF000000</color>
+ <color name="white">#FFFFFFFF</color>
+</resources> \ 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..d94d5d7
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="ic_launcher_background">#CFCFCF</color>
+</resources> \ 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..3bb55dc
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+ <string name="app_name">MineGame</string>
+</resources> \ 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..d16bacc
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Theme.Chat" parent="android:Theme.Material.Light.NoActionBar" />
+</resources> \ 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..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -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 that 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> \ 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 @@
+<?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> \ No newline at end of file
diff --git a/app/src/test/java/com/a404m/mine_game/ExampleUnitTest.kt b/app/src/test/java/com/a404m/mine_game/ExampleUnitTest.kt
new file mode 100644
index 0000000..0583dd0
--- /dev/null
+++ b/app/src/test/java/com/a404m/mine_game/ExampleUnitTest.kt
@@ -0,0 +1,20 @@
+package com.a404m.mine_game
+
+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..952b930
--- /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.compose) 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..e1aa9aa
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,40 @@
+[versions]
+agp = "8.11.0"
+coilCompose = "2.2.2"
+kotlin = "2.0.21"
+coreKtx = "1.16.0"
+junit = "4.13.2"
+junitVersion = "1.2.1"
+espressoCore = "3.6.1"
+kotlinxCoroutinesAndroid = "1.8.1"
+lifecycleRuntimeKtx = "2.9.1"
+activityCompose = "1.10.1"
+composeBom = "2025.06.01"
+navigationCompose = "2.9.0"
+okhttp = "4.12.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
+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-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
+okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e95e51b
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Nov 25 22:44:06 IRST 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-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..92631b6
--- /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()
+ }
+}
+
+rootProject.name = "MineGame"
+include(":app")
+ \ No newline at end of file