Merge pull request #159 from lucasnlm/add-support-screen

Add support screen
This commit is contained in:
Lucas Nunes 2020-08-20 00:35:21 -03:00 committed by GitHub
commit 388bcdcfce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
199 changed files with 3112 additions and 1353 deletions

2
.gitignore vendored
View file

@ -12,3 +12,5 @@ app/local.properties
app/release/ app/release/
app/standalone/ app/standalone/
app/google/ app/google/
app/google-services.json
proprietary/google-services.json

View file

@ -66,7 +66,7 @@ Where `ANDROID_JRE` is the Java runtime provided by Android Studio.
- [Android SDK 30](https://developer.android.com/about/versions/11) - [Android SDK 30](https://developer.android.com/about/versions/11)
- [AndroidX](https://developer.android.com/jetpack/androidx) - [AndroidX](https://developer.android.com/jetpack/androidx)
- [Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle) - [Lifecycle](https://developer.android.com/topic/libraries/architecture/lifecycle)
- [Dagger Hilt](https://dagger.dev/hilt/) - [Koin Kotlin](https://github.com/InsertKoinIO/koin)
- [Room](https://developer.android.com/training/data-storage/room) - [Room](https://developer.android.com/training/data-storage/room)
- [Robolectric](http://robolectric.org/) - [Robolectric](http://robolectric.org/)
- [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) - [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)

View file

@ -1,7 +1,6 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {
@ -9,9 +8,9 @@ android {
defaultConfig { defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid // versionCode and versionName must be hardcoded to support F-droid
versionCode 800041 versionCode 800051
versionName '8.0.4' versionName '8.0.5'
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
multiDexEnabled true multiDexEnabled true
vectorDrawables.useSupportLibrary true vectorDrawables.useSupportLibrary true
@ -85,14 +84,11 @@ kapt {
correctErrorTypes true correctErrorTypes true
} }
hilt {
enableTransformForLocalTests true
}
dependencies { dependencies {
// Dependencies must be hardcoded to support F-droid // Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':external')
implementation project(':common') implementation project(':common')
googleImplementation project(':proprietary') googleImplementation project(':proprietary')
@ -119,15 +115,10 @@ dependencies {
// Google // Google
implementation 'com.google.android.material:material:1.2.0' implementation 'com.google.android.material:material:1.2.0'
// Dagger // Koin
implementation 'com.google.dagger:hilt-android:2.28.1-alpha' implementation 'org.koin:koin-android:2.1.6'
kapt 'com.google.dagger:hilt-android-compiler:2.28.1-alpha' implementation 'org.koin:koin-androidx-viewmodel:2.1.6'
testImplementation 'com.google.dagger:hilt-android-testing:2.28.1-alpha' testImplementation 'org.koin:koin-test:2.1.6'
kaptTest 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.28.1-alpha'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
// Kotlin // Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'

View file

@ -106,7 +106,6 @@ class GameFlowTests {
.perform(NavigationViewActions.navigateTo(R.id.control)) .perform(NavigationViewActions.navigateTo(R.id.control))
onView(withText(R.string.standard)).perform(click()) onView(withText(R.string.standard)).perform(click())
onView(withText(R.string.flag_first)).perform(click())
onView(withText(R.string.ok)).perform(click()) onView(withText(R.string.ok)).perform(click())
} }

View file

@ -1,2 +0,0 @@
sdk=28
application=dagger.hilt.android.testing.HiltTestApplication

View file

@ -12,6 +12,4 @@ object DeepLink {
const val EXPERT_PATH = "expert" const val EXPERT_PATH = "expert"
const val STANDARD_PATH = "standard" const val STANDARD_PATH = "standard"
const val CUSTOM_PATH = "custom" const val CUSTOM_PATH = "custom"
const val CUSTOM_NEW_GAME = "antimine://new-game/custom"
} }

View file

@ -9,20 +9,20 @@ import android.os.Handler
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import android.widget.FrameLayout
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.HandlerCompat.postDelayed import androidx.core.os.HandlerCompat.postDelayed
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.doOnLayout
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.about.AboutActivity import dev.lucasnlm.antimine.about.AboutActivity
import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.models.Event
@ -36,13 +36,15 @@ import dev.lucasnlm.antimine.core.analytics.models.Analytics
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.custom.CustomLevelDialogFragment import dev.lucasnlm.antimine.custom.CustomLevelDialogFragment
import dev.lucasnlm.antimine.history.HistoryActivity import dev.lucasnlm.antimine.history.HistoryActivity
import dev.lucasnlm.antimine.instant.InstantAppManager
import dev.lucasnlm.antimine.level.view.EndGameDialogFragment import dev.lucasnlm.antimine.level.view.EndGameDialogFragment
import dev.lucasnlm.antimine.level.view.LevelFragment import dev.lucasnlm.antimine.level.view.LevelFragment
import dev.lucasnlm.antimine.playgames.PlayGamesDialogFragment
import dev.lucasnlm.antimine.preferences.PreferencesActivity import dev.lucasnlm.antimine.preferences.PreferencesActivity
import dev.lucasnlm.antimine.share.viewmodel.ShareViewModel import dev.lucasnlm.antimine.share.ShareManager
import dev.lucasnlm.antimine.stats.StatsActivity import dev.lucasnlm.antimine.stats.StatsActivity
import dev.lucasnlm.antimine.theme.ThemeActivity import dev.lucasnlm.antimine.theme.ThemeActivity
import dev.lucasnlm.external.IInstantAppManager
import dev.lucasnlm.external.IPlayGamesManager
import kotlinx.android.synthetic.main.activity_game.* import kotlinx.android.synthetic.main.activity_game.*
import kotlinx.android.synthetic.main.activity_game.minesCount import kotlinx.android.synthetic.main.activity_game.minesCount
import kotlinx.android.synthetic.main.activity_game.timer import kotlinx.android.synthetic.main.activity_game.timer
@ -50,24 +52,23 @@ import kotlinx.android.synthetic.main.activity_tv_game.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.OnDismissListener { class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.OnDismissListener {
@Inject private val preferencesRepository: IPreferencesRepository by inject()
lateinit var preferencesRepository: IPreferencesRepository
@Inject private val analyticsManager: IAnalyticsManager by inject()
lateinit var analyticsManager: IAnalyticsManager
@Inject private val instantAppManager: IInstantAppManager by inject()
lateinit var instantAppManager: InstantAppManager
@Inject private val savesRepository: ISavesRepository by inject()
lateinit var savesRepository: ISavesRepository
val viewModel: GameViewModel by viewModels() private val playGamesManager: IPlayGamesManager by inject()
private val shareViewModel: ShareViewModel by viewModels()
private val shareViewModel: ShareManager by inject()
val gameViewModel by viewModel<GameViewModel>()
override val noActionBar: Boolean = true override val noActionBar: Boolean = true
@ -90,9 +91,12 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
bindToolbar() bindToolbar()
bindDrawer() bindDrawer()
bindNavigationMenu() bindNavigationMenu()
loadGameFragment()
if (instantAppManager.isEnabled()) { findViewById<FrameLayout>(R.id.levelContainer).doOnLayout {
loadGameFragment()
}
if (instantAppManager.isEnabled(applicationContext)) {
bindInstantApp() bindInstantApp()
savesRepository.setLimit(1) savesRepository.setLimit(1)
} else { } else {
@ -100,7 +104,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
} }
private fun bindViewModel() = viewModel.apply { private fun bindViewModel() = gameViewModel.apply {
var lastEvent: Event? = null // TODO use distinctUntilChanged when available var lastEvent: Event? = null // TODO use distinctUntilChanged when available
eventObserver.observe( eventObserver.observe(
@ -117,7 +121,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
this@GameActivity, this@GameActivity,
Observer { Observer {
lifecycleScope.launch { lifecycleScope.launch {
viewModel.retryGame(currentSaveId.toInt()) gameViewModel.retryGame(currentSaveId.toInt())
} }
} }
) )
@ -179,9 +183,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
when { when {
drawer.isDrawerOpen(GravityCompat.START) -> { drawer.isDrawerOpen(GravityCompat.START) -> {
drawer.closeDrawer(GravityCompat.START) drawer.closeDrawer(GravityCompat.START)
viewModel.resumeGame() gameViewModel.resumeGame()
} }
status == Status.Running && instantAppManager.isEnabled() -> showQuitConfirmation { status == Status.Running && instantAppManager.isEnabled(applicationContext) -> showQuitConfirmation {
super.onBackPressed() super.onBackPressed()
} }
else -> super.onBackPressed() else -> super.onBackPressed()
@ -194,13 +198,15 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (!willReset) { if (!willReset) {
if (status == Status.Running) { if (status == Status.Running) {
viewModel.run { gameViewModel.run {
refreshUserPreferences() refreshUserPreferences()
resumeGame() resumeGame()
} }
analyticsManager.sentEvent(Analytics.Resume) analyticsManager.sentEvent(Analytics.Resume)
} }
silentGooglePlayLogin()
} }
} }
@ -208,7 +214,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
super.onPause() super.onPause()
if (status == Status.Running) { if (status == Status.Running) {
viewModel.pauseGame() gameViewModel.pauseGame()
} }
if (isFinishing) { if (isFinishing) {
@ -242,12 +248,12 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (confirmResign) { if (confirmResign) {
newGameConfirmation { newGameConfirmation {
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame() gameViewModel.startNewGame()
} }
} }
} else { } else {
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame() gameViewModel.startNewGame()
} }
} }
} }
@ -284,13 +290,13 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
override fun onDrawerOpened(drawerView: View) { override fun onDrawerOpened(drawerView: View) {
viewModel.pauseGame() gameViewModel.pauseGame()
analyticsManager.sentEvent(Analytics.OpenDrawer) analyticsManager.sentEvent(Analytics.OpenDrawer)
} }
override fun onDrawerClosed(drawerView: View) { override fun onDrawerClosed(drawerView: View) {
if (hasNoOtherFocusedDialog()) { if (hasNoOtherFocusedDialog()) {
viewModel.resumeGame() gameViewModel.resumeGame()
} }
analyticsManager.sentEvent(Analytics.CloseDrawer) analyticsManager.sentEvent(Analytics.CloseDrawer)
@ -301,9 +307,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
}) })
if (preferencesRepository.getBoolean(PREFERENCE_FIRST_USE, false)) { if (preferencesRepository.isFirstUse()) {
openDrawer(GravityCompat.START) openDrawer(GravityCompat.START)
preferencesRepository.putBoolean(PREFERENCE_FIRST_USE, true) preferencesRepository.completeFirstUse()
} }
} }
} }
@ -327,6 +333,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
R.id.previous_games -> openSaveHistory() R.id.previous_games -> openSaveHistory()
R.id.stats -> openStats() R.id.stats -> openStats()
R.id.install_new -> installFromInstantApp() R.id.install_new -> installFromInstantApp()
R.id.play_games -> googlePlay()
else -> handled = false else -> handled = false
} }
@ -337,19 +344,23 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
handled handled
} }
navigationView.menu.findItem(R.id.share_now).isVisible = instantAppManager.isNotEnabled() navigationView.menu.findItem(R.id.share_now).isVisible = !instantAppManager.isEnabled(applicationContext)
if (!playGamesManager.hasGooglePlayGames()) {
navigationView.menu.removeGroup(R.id.play_games_group)
}
} }
private fun checkUseCount() { private fun checkUseCount() {
val current = preferencesRepository.getInt(PREFERENCE_USE_COUNT, 0) val current = preferencesRepository.getUseCount()
val shouldRequestRating = preferencesRepository.getBoolean(PREFERENCE_REQUEST_RATING, true) val shouldRequestRating = preferencesRepository.isRequestRatingEnabled()
if (current >= MIN_USAGES_TO_RATING && shouldRequestRating) { if (current >= MIN_USAGES_TO_RATING && shouldRequestRating) {
analyticsManager.sentEvent(Analytics.ShowRatingRequest(current)) analyticsManager.sentEvent(Analytics.ShowRatingRequest(current))
showRequestRating() showRequestRating()
} }
preferencesRepository.putInt(PREFERENCE_USE_COUNT, current + 1) preferencesRepository.incrementUseCount()
} }
private fun onChangeDifficulty(difficulty: Difficulty) { private fun onChangeDifficulty(difficulty: Difficulty) {
@ -369,22 +380,22 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
private fun loadGameFragment() { private fun loadGameFragment() {
val fragmentManager = supportFragmentManager supportFragmentManager.apply {
popBackStack()
fragmentManager.popBackStack() findFragmentById(R.id.levelContainer)?.let { it ->
beginTransaction().apply {
remove(it)
commitAllowingStateLoss()
}
}
fragmentManager.findFragmentById(R.id.levelContainer)?.let { it -> beginTransaction().apply {
fragmentManager.beginTransaction().apply { replace(R.id.levelContainer, LevelFragment())
remove(it) setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
commitAllowingStateLoss() commitAllowingStateLoss()
} }
} }
fragmentManager.beginTransaction().apply {
replace(R.id.levelContainer, LevelFragment())
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
commitAllowingStateLoss()
}
} }
private fun showRequestRating() { private fun showRequestRating() {
@ -397,7 +408,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
openRateUsLink("Dialog") openRateUsLink("Dialog")
} }
.setNegativeButton(R.string.rating_button_no) { _, _ -> .setNegativeButton(R.string.rating_button_no) { _, _ ->
preferencesRepository.putBoolean(PREFERENCE_REQUEST_RATING, false) preferencesRepository.disableRequestRating()
} }
.show() .show()
} }
@ -431,7 +442,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
private fun showControlDialog() { private fun showControlDialog() {
viewModel.pauseGame() gameViewModel.pauseGame()
if (supportFragmentManager.findFragmentByTag(CustomLevelDialogFragment.TAG) == null) { if (supportFragmentManager.findFragmentByTag(CustomLevelDialogFragment.TAG) == null) {
ControlDialogFragment().apply { ControlDialogFragment().apply {
@ -493,13 +504,13 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
private fun waitAndShowEndGameDialog(victory: Boolean, await: Boolean) { private fun waitAndShowEndGameDialog(victory: Boolean, await: Boolean) {
if (await && viewModel.explosionDelay() != 0L) { if (await && gameViewModel.explosionDelay() != 0L) {
postDelayed( postDelayed(
Handler(), Handler(),
{ {
showEndGameDialog(victory) showEndGameDialog(victory)
}, },
null, (viewModel.explosionDelay() * 0.3).toLong() null, (gameViewModel.explosionDelay() * 0.3).toLong()
) )
} else { } else {
showEndGameDialog(victory) showEndGameDialog(victory)
@ -509,12 +520,12 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
private fun changeDifficulty(newDifficulty: Difficulty) { private fun changeDifficulty(newDifficulty: Difficulty) {
if (status == Status.PreGame) { if (status == Status.PreGame) {
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame(newDifficulty) gameViewModel.startNewGame(newDifficulty)
} }
} else { } else {
newGameConfirmation { newGameConfirmation {
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame(newDifficulty) gameViewModel.startNewGame(newDifficulty)
} }
} }
} }
@ -532,7 +543,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
Event.Resume, Event.Running -> { Event.Resume, Event.Running -> {
status = Status.Running status = Status.Running
viewModel.runClock() gameViewModel.runClock()
refreshNewGameButton() refreshNewGameButton()
keepScreenOn(true) keepScreenOn(true)
} }
@ -543,9 +554,9 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
totalArea totalArea
) )
status = Status.Over(currentTime, score) status = Status.Over(currentTime, score)
viewModel.stopClock() gameViewModel.stopClock()
viewModel.revealAllEmptyAreas() gameViewModel.revealAllEmptyAreas()
viewModel.victory() gameViewModel.victory()
refreshNewGameButton() refreshNewGameButton()
keepScreenOn(false) keepScreenOn(false)
waitAndShowEndGameDialog( waitAndShowEndGameDialog(
@ -563,10 +574,10 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
status = Status.Over(currentTime, score) status = Status.Over(currentTime, score)
refreshNewGameButton() refreshNewGameButton()
keepScreenOn(false) keepScreenOn(false)
viewModel.stopClock() gameViewModel.stopClock()
GlobalScope.launch(context = Dispatchers.Main) { GlobalScope.launch(context = Dispatchers.Main) {
viewModel.gameOver(isResuming) gameViewModel.gameOver(isResuming)
waitAndShowEndGameDialog( waitAndShowEndGameDialog(
victory = false, victory = false,
await = true await = true
@ -590,10 +601,10 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
private fun shareCurrentGame() { private fun shareCurrentGame() {
val levelSetup = viewModel.levelSetup.value val levelSetup = gameViewModel.levelSetup.value
val field = viewModel.field.value val field = gameViewModel.field.value
GlobalScope.launch { lifecycleScope.launch {
shareViewModel.share(levelSetup, field) shareViewModel.shareField(levelSetup, field)
} }
} }
@ -625,7 +636,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
analyticsManager.sentEvent(Analytics.TapRatingRequest(from)) analyticsManager.sentEvent(Analytics.TapRatingRequest(from))
preferencesRepository.putBoolean(PREFERENCE_REQUEST_RATING, false) preferencesRepository.disableRequestRating()
} }
private fun keepScreenOn(enabled: Boolean) { private fun keepScreenOn(enabled: Boolean) {
@ -643,19 +654,42 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} }
override fun onDismiss(dialog: DialogInterface?) { override fun onDismiss(dialog: DialogInterface?) {
viewModel.run { gameViewModel.run {
refreshUserPreferences() refreshUserPreferences()
resumeGame() resumeGame()
} }
} }
companion object { private fun silentGooglePlayLogin() {
const val PREFERENCE_FIRST_USE = "preference_first_use" if (playGamesManager.hasGooglePlayGames()) {
const val PREFERENCE_USE_COUNT = "preference_use_count" playGamesManager.silentLogin(this)
const val PREFERENCE_REQUEST_RATING = "preference_request_rating" invalidateOptionsMenu()
}
}
private fun googlePlay() {
if (playGamesManager.isLogged()) {
PlayGamesDialogFragment().show(supportFragmentManager, PlayGamesDialogFragment.TAG)
} else {
playGamesManager.getLoginIntent()?.let {
startActivityForResult(it, GOOGLE_PLAY_REQUEST_CODE)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == GOOGLE_PLAY_REQUEST_CODE) {
playGamesManager.handleLoginResult(data)
invalidateOptionsMenu()
}
}
companion object {
const val IA_REFERRER = "InstallApiActivity" const val IA_REFERRER = "InstallApiActivity"
const val IA_REQUEST_CODE = 5 const val IA_REQUEST_CODE = 5
const val GOOGLE_PLAY_REQUEST_CODE = 6
const val MIN_USAGES_TO_RATING = 4 const val MIN_USAGES_TO_RATING = 4
} }

View file

@ -1,18 +1,26 @@
package dev.lucasnlm.antimine package dev.lucasnlm.antimine
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import dagger.hilt.android.HiltAndroidApp import dev.lucasnlm.antimine.common.level.di.LevelModule
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.models.Analytics import dev.lucasnlm.antimine.core.analytics.models.Analytics
import javax.inject.Inject import dev.lucasnlm.antimine.core.di.CommonModule
import dev.lucasnlm.antimine.di.AppModule
import dev.lucasnlm.antimine.di.ViewModelModule
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
@HiltAndroidApp
open class MainApplication : MultiDexApplication() { open class MainApplication : MultiDexApplication() {
@Inject private val analyticsManager: IAnalyticsManager by inject()
lateinit var analyticsManager: IAnalyticsManager
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startKoin {
androidContext(applicationContext)
modules(AppModule, CommonModule, LevelModule, ViewModelModule)
}
analyticsManager.apply { analyticsManager.apply {
setup(applicationContext, mapOf()) setup(applicationContext, mapOf())
sentEvent(Analytics.Open) sentEvent(Analytics.Open)

View file

@ -5,14 +5,11 @@ import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import dev.lucasnlm.antimine.core.themes.model.AppTheme import dev.lucasnlm.antimine.core.themes.model.AppTheme
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import javax.inject.Inject import org.koin.android.ext.android.inject
open class ThematicActivity : AppCompatActivity { open class ThematicActivity(@LayoutRes contentLayoutId: Int) : AppCompatActivity(contentLayoutId) {
constructor() : super()
constructor(@LayoutRes contentLayoutId: Int) : super(contentLayoutId)
@Inject private val themeRepository: IThemeRepository by inject()
lateinit var themeRepository: IThemeRepository
protected open val noActionBar: Boolean = false protected open val noActionBar: Boolean = false

View file

@ -5,14 +5,12 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.HandlerCompat import androidx.core.os.HandlerCompat
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.about.AboutActivity import dev.lucasnlm.antimine.about.AboutActivity
import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.models.Event
@ -26,10 +24,10 @@ import kotlinx.android.synthetic.main.activity_tv_game.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class TvGameActivity : AppCompatActivity() { class TvGameActivity : AppCompatActivity() {
private val viewModel: GameViewModel by viewModels() private val gameViewModel by viewModel<GameViewModel>()
private var status: Status = Status.PreGame private var status: Status = Status.PreGame
private var totalMines: Int = 0 private var totalMines: Int = 0
@ -49,7 +47,7 @@ class TvGameActivity : AppCompatActivity() {
loadGameFragment() loadGameFragment()
} }
private fun bindViewModel() = viewModel.apply { private fun bindViewModel() = gameViewModel.apply {
eventObserver.observe( eventObserver.observe(
this@TvGameActivity, this@TvGameActivity,
Observer { Observer {
@ -99,7 +97,7 @@ class TvGameActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (status == Status.Running) { if (status == Status.Running) {
viewModel.resumeGame() gameViewModel.resumeGame()
} }
} }
@ -107,7 +105,7 @@ class TvGameActivity : AppCompatActivity() {
super.onPause() super.onPause()
if (status == Status.Running) { if (status == Status.Running) {
viewModel.pauseGame() gameViewModel.pauseGame()
} }
} }
@ -174,7 +172,7 @@ class TvGameActivity : AppCompatActivity() {
setCancelable(false) setCancelable(false)
setPositiveButton(R.string.new_game) { _, _ -> setPositiveButton(R.string.new_game) { _, _ ->
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame() gameViewModel.startNewGame()
} }
} }
setNegativeButton(R.string.cancel, null) setNegativeButton(R.string.cancel, null)
@ -193,7 +191,7 @@ class TvGameActivity : AppCompatActivity() {
setMessage(R.string.new_game_request) setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ -> setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame() gameViewModel.startNewGame()
} }
} }
setNegativeButton(R.string.cancel, null) setNegativeButton(R.string.cancel, null)
@ -217,7 +215,7 @@ class TvGameActivity : AppCompatActivity() {
setMessage(R.string.new_game_request) setMessage(R.string.new_game_request)
setPositiveButton(R.string.yes) { _, _ -> setPositiveButton(R.string.yes) { _, _ ->
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame() gameViewModel.startNewGame()
} }
} }
setNegativeButton(R.string.cancel, null) setNegativeButton(R.string.cancel, null)
@ -231,12 +229,12 @@ class TvGameActivity : AppCompatActivity() {
private fun changeDifficulty(newDifficulty: Difficulty) { private fun changeDifficulty(newDifficulty: Difficulty) {
if (status == Status.PreGame) { if (status == Status.PreGame) {
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame(newDifficulty) gameViewModel.startNewGame(newDifficulty)
} }
} else { } else {
newGameConfirmation { newGameConfirmation {
GlobalScope.launch { GlobalScope.launch {
viewModel.startNewGame(newDifficulty) gameViewModel.startNewGame(newDifficulty)
} }
} }
} }
@ -253,7 +251,7 @@ class TvGameActivity : AppCompatActivity() {
} }
Event.Resume, Event.Running -> { Event.Resume, Event.Running -> {
status = Status.Running status = Status.Running
viewModel.runClock() gameViewModel.runClock()
invalidateOptionsMenu() invalidateOptionsMenu()
} }
Event.Victory -> { Event.Victory -> {
@ -263,8 +261,8 @@ class TvGameActivity : AppCompatActivity() {
totalArea totalArea
) )
status = Status.Over(currentTime, score) status = Status.Over(currentTime, score)
viewModel.stopClock() gameViewModel.stopClock()
viewModel.revealAllEmptyAreas() gameViewModel.revealAllEmptyAreas()
invalidateOptionsMenu() invalidateOptionsMenu()
showVictory() showVictory()
} }
@ -276,10 +274,10 @@ class TvGameActivity : AppCompatActivity() {
) )
status = Status.Over(currentTime, score) status = Status.Over(currentTime, score)
invalidateOptionsMenu() invalidateOptionsMenu()
viewModel.stopClock() gameViewModel.stopClock()
GlobalScope.launch(context = Dispatchers.Main) { GlobalScope.launch(context = Dispatchers.Main) {
viewModel.gameOver(false) gameViewModel.gameOver(false)
waitAndShowGameOverConfirmNewGame() waitAndShowGameOverConfirmNewGame()
} }
} }
@ -291,7 +289,7 @@ class TvGameActivity : AppCompatActivity() {
) )
status = Status.Over(currentTime, score) status = Status.Over(currentTime, score)
invalidateOptionsMenu() invalidateOptionsMenu()
viewModel.stopClock() gameViewModel.stopClock()
waitAndShowConfirmNewGame() waitAndShowConfirmNewGame()
} }

View file

@ -1,23 +1,21 @@
package dev.lucasnlm.antimine.about package dev.lucasnlm.antimine.about
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.about.viewmodel.AboutEvent import dev.lucasnlm.antimine.about.viewmodel.AboutEvent
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import dev.lucasnlm.antimine.about.views.AboutInfoFragment import dev.lucasnlm.antimine.about.views.info.AboutInfoFragment
import dev.lucasnlm.antimine.about.views.licenses.LicensesFragment import dev.lucasnlm.antimine.about.views.licenses.LicensesFragment
import dev.lucasnlm.antimine.about.views.translators.TranslatorsFragment import dev.lucasnlm.antimine.about.views.translators.TranslatorsFragment
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class AboutActivity : ThematicActivity(R.layout.activity_empty) { class AboutActivity : ThematicActivity(R.layout.activity_empty) {
private val aboutViewModel: AboutViewModel by viewModels() private val aboutViewModel: AboutViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -3,13 +3,11 @@ package dev.lucasnlm.antimine.about.viewmodel
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.hilt.lifecycle.ViewModelInject
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
class AboutViewModel @ViewModelInject constructor( class AboutViewModel(
@ApplicationContext private val context: Context private val context: Context
) : IntentViewModel<AboutEvent, AboutState>() { ) : IntentViewModel<AboutEvent, AboutState>() {
override fun onEvent(event: AboutEvent) { override fun onEvent(event: AboutEvent) {
@ -47,10 +45,11 @@ class AboutViewModel @ViewModelInject constructor(
private fun getLicensesList() = mapOf( private fun getLicensesList() = mapOf(
"Android SDK License" to R.raw.android_sdk, "Android SDK License" to R.raw.android_sdk,
"Material Design Icons" to R.raw.apache2, "Material Design" to R.raw.apache2,
"Dagger Hilt" to R.raw.apache2, "Koin" to R.raw.apache2,
"Moshi" to R.raw.apache2, "Moshi" to R.raw.apache2,
"Mockito" to R.raw.mockito, "Mockito" to R.raw.mockito,
"Noto Emoji" to R.raw.apache2,
"Sounds" to R.raw.sounds "Sounds" to R.raw.sounds
).map { ).map {
License(it.key, it.value) License(it.key, it.value)

View file

@ -1,17 +1,17 @@
package dev.lucasnlm.antimine.about.views package dev.lucasnlm.antimine.about.views.info
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import dev.lucasnlm.antimine.BuildConfig import dev.lucasnlm.antimine.BuildConfig
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.viewmodel.AboutEvent import dev.lucasnlm.antimine.about.viewmodel.AboutEvent
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import kotlinx.android.synthetic.main.fragment_about_info.* import kotlinx.android.synthetic.main.fragment_about_info.*
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class AboutInfoFragment : Fragment(R.layout.fragment_about_info) { class AboutInfoFragment : Fragment(R.layout.fragment_about_info) {
private val aboutViewModel: AboutViewModel by activityViewModels() private val aboutViewModel: AboutViewModel by sharedViewModel()
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()

View file

@ -3,7 +3,6 @@ package dev.lucasnlm.antimine.about.views.licenses
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -11,9 +10,10 @@ import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import kotlinx.android.synthetic.main.fragment_licenses.* import kotlinx.android.synthetic.main.fragment_licenses.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class LicensesFragment : Fragment(R.layout.fragment_licenses) { class LicensesFragment : Fragment(R.layout.fragment_licenses) {
private val aboutViewModel: AboutViewModel by activityViewModels() private val aboutViewModel: AboutViewModel by sharedViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View file

@ -4,7 +4,6 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -14,9 +13,10 @@ import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import kotlinx.android.synthetic.main.fragment_translators.* import kotlinx.android.synthetic.main.fragment_translators.*
import kotlinx.android.synthetic.main.view_translator.view.* import kotlinx.android.synthetic.main.view_translator.view.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
internal class TranslatorsFragment : Fragment(R.layout.fragment_translators) { internal class TranslatorsFragment : Fragment(R.layout.fragment_translators) {
private val aboutViewModel: AboutViewModel by activityViewModels() private val aboutViewModel: AboutViewModel by sharedViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View file

@ -8,23 +8,21 @@ import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.control.view.ControlItemView import dev.lucasnlm.antimine.control.view.ControlItemView
import dev.lucasnlm.antimine.control.viewmodel.ControlEvent import dev.lucasnlm.antimine.control.viewmodel.ControlEvent
import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class ControlDialogFragment : AppCompatDialogFragment() { class ControlDialogFragment : AppCompatDialogFragment() {
private val controlViewModel by activityViewModels<ControlViewModel>() private val controlViewModel by viewModel<ControlViewModel>()
private val adapter by lazy { ControlListAdapter(controlViewModel) } private val adapter by lazy { ControlListAdapter(controlViewModel) }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val currentControl = controlViewModel.singleState().selectedId val state = controlViewModel.singleState()
return AlertDialog.Builder(requireContext()).apply { return AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.control) setTitle(R.string.control)
setSingleChoiceItems(adapter, currentControl, null) setSingleChoiceItems(adapter, state.selectedIndex, null)
setPositiveButton(R.string.ok, null) setPositiveButton(R.string.ok, null)
}.create() }.create()
} }
@ -41,8 +39,6 @@ class ControlDialogFragment : AppCompatDialogFragment() {
) : BaseAdapter() { ) : BaseAdapter() {
private val controlList = controlViewModel.singleState().gameControls private val controlList = controlViewModel.singleState().gameControls
fun getSelectedId() = controlViewModel.singleState().selectedId
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = if (convertView == null) { val view = if (convertView == null) {
ControlItemView(parent!!.context) ControlItemView(parent!!.context)
@ -50,12 +46,12 @@ class ControlDialogFragment : AppCompatDialogFragment() {
(convertView as ControlItemView) (convertView as ControlItemView)
} }
val selectedId = getSelectedId() val selected = controlViewModel.singleState().selected
return view.apply { return view.apply {
val controlModel = controlList[position] val controlModel = controlList[position]
bind(controlModel) bind(controlModel)
setRadio(selectedId == controlModel.controlStyle.ordinal) setRadio(selected == controlModel.controlStyle)
setOnClickListener { setOnClickListener {
controlViewModel.sendEvent(ControlEvent.SelectControlStyle(controlModel.controlStyle)) controlViewModel.sendEvent(ControlEvent.SelectControlStyle(controlModel.controlStyle))
notifyDataSetChanged() notifyDataSetChanged()

View file

@ -6,7 +6,6 @@ import dev.lucasnlm.antimine.core.control.ControlStyle
data class ControlDetails( data class ControlDetails(
val id: Long, val id: Long,
val controlStyle: ControlStyle, val controlStyle: ControlStyle,
@StringRes val titleId: Int,
@StringRes val firstActionId: Int, @StringRes val firstActionId: Int,
@StringRes val firstActionResponseId: Int, @StringRes val firstActionResponseId: Int,
@StringRes val secondActionId: Int, @StringRes val secondActionId: Int,

View file

@ -17,7 +17,6 @@ class ControlItemView : FrameLayout {
private val radio: AppCompatRadioButton private val radio: AppCompatRadioButton
private val root: View private val root: View
private val title: TextView
private val firstAction: TextView private val firstAction: TextView
private val firstActionResponse: TextView private val firstActionResponse: TextView
private val secondAction: TextView private val secondAction: TextView
@ -30,7 +29,6 @@ class ControlItemView : FrameLayout {
radio = findViewById(R.id.radio) radio = findViewById(R.id.radio)
root = findViewById(R.id.root) root = findViewById(R.id.root)
title = findViewById(R.id.title)
firstAction = findViewById(R.id.firstAction) firstAction = findViewById(R.id.firstAction)
firstActionResponse = findViewById(R.id.firstActionResponse) firstActionResponse = findViewById(R.id.firstActionResponse)
secondAction = findViewById(R.id.secondAction) secondAction = findViewById(R.id.secondAction)
@ -38,7 +36,6 @@ class ControlItemView : FrameLayout {
} }
fun bind(controlDetails: ControlDetails) { fun bind(controlDetails: ControlDetails) {
title.text = context.getString(controlDetails.titleId)
firstAction.text = context.getString(controlDetails.firstActionId) firstAction.text = context.getString(controlDetails.firstActionId)
firstActionResponse.text = context.getString(controlDetails.firstActionResponseId) firstActionResponse.text = context.getString(controlDetails.firstActionResponseId)
secondAction.text = context.getString(controlDetails.secondActionId) secondAction.text = context.getString(controlDetails.secondActionId)

View file

@ -1,8 +1,10 @@
package dev.lucasnlm.antimine.control.viewmodel package dev.lucasnlm.antimine.control.viewmodel
import dev.lucasnlm.antimine.control.models.ControlDetails import dev.lucasnlm.antimine.control.models.ControlDetails
import dev.lucasnlm.antimine.core.control.ControlStyle
data class ControlState( data class ControlState(
val selectedId: Int, val selectedIndex: Int,
val selected: ControlStyle,
val gameControls: List<ControlDetails> val gameControls: List<ControlDetails>
) )

View file

@ -1,6 +1,5 @@
package dev.lucasnlm.antimine.control.viewmodel package dev.lucasnlm.antimine.control.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.control.models.ControlDetails import dev.lucasnlm.antimine.control.models.ControlDetails
import dev.lucasnlm.antimine.core.control.ControlStyle import dev.lucasnlm.antimine.core.control.ControlStyle
@ -8,7 +7,7 @@ import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
class ControlViewModel @ViewModelInject constructor( class ControlViewModel(
private val preferencesRepository: IPreferencesRepository private val preferencesRepository: IPreferencesRepository
) : IntentViewModel<ControlEvent, ControlState>() { ) : IntentViewModel<ControlEvent, ControlState>() {
@ -16,7 +15,6 @@ class ControlViewModel @ViewModelInject constructor(
ControlDetails( ControlDetails(
id = 0L, id = 0L,
controlStyle = ControlStyle.Standard, controlStyle = ControlStyle.Standard,
titleId = R.string.standard,
firstActionId = R.string.single_click, firstActionId = R.string.single_click,
firstActionResponseId = R.string.open_tile, firstActionResponseId = R.string.open_tile,
secondActionId = R.string.long_press, secondActionId = R.string.long_press,
@ -25,7 +23,6 @@ class ControlViewModel @ViewModelInject constructor(
ControlDetails( ControlDetails(
id = 1L, id = 1L,
controlStyle = ControlStyle.FastFlag, controlStyle = ControlStyle.FastFlag,
titleId = R.string.flag_first,
firstActionId = R.string.single_click, firstActionId = R.string.single_click,
firstActionResponseId = R.string.flag_tile, firstActionResponseId = R.string.flag_tile,
secondActionId = R.string.long_press, secondActionId = R.string.long_press,
@ -34,29 +31,42 @@ class ControlViewModel @ViewModelInject constructor(
ControlDetails( ControlDetails(
id = 2L, id = 2L,
controlStyle = ControlStyle.DoubleClick, controlStyle = ControlStyle.DoubleClick,
titleId = R.string.double_click,
firstActionId = R.string.single_click, firstActionId = R.string.single_click,
firstActionResponseId = R.string.flag_tile, firstActionResponseId = R.string.flag_tile,
secondActionId = R.string.double_click, secondActionId = R.string.double_click,
secondActionResponseId = R.string.open_tile secondActionResponseId = R.string.open_tile
),
ControlDetails(
id = 3L,
controlStyle = ControlStyle.DoubleClickInverted,
firstActionId = R.string.single_click,
firstActionResponseId = R.string.open_tile,
secondActionId = R.string.double_click,
secondActionResponseId = R.string.flag_tile
) )
) )
override fun initialState(): ControlState = override fun initialState(): ControlState {
ControlState( val controlDetails = gameControlOptions.firstOrNull {
selectedId = gameControlOptions.firstOrNull { it.controlStyle == preferencesRepository.controlStyle()
it.controlStyle == preferencesRepository.controlStyle() }
}?.id?.toInt() ?: 0, return ControlState(
selectedIndex = controlDetails?.id?.toInt() ?: 0,
selected = controlDetails?.controlStyle ?: ControlStyle.Standard,
gameControls = gameControlOptions gameControls = gameControlOptions
) )
}
override suspend fun mapEventToState(event: ControlEvent) = flow { override suspend fun mapEventToState(event: ControlEvent) = flow {
if (event is ControlEvent.SelectControlStyle) { if (event is ControlEvent.SelectControlStyle) {
val controlStyle = event.controlStyle val controlStyle = event.controlStyle
preferencesRepository.useControlStyle(controlStyle) preferencesRepository.useControlStyle(controlStyle)
val selected = state.gameControls.first { it.controlStyle == event.controlStyle }
val newState = state.copy( val newState = state.copy(
selectedId = state.gameControls.first { it.controlStyle == event.controlStyle }.id.toInt() selectedIndex = selected.id.toInt(),
selected = selected.controlStyle
) )
emit(newState) emit(newState)

View file

@ -1,60 +1,67 @@
package dev.lucasnlm.antimine.custom package dev.lucasnlm.antimine.custom
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.activityViewModels
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Minefield import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.custom.viewmodel.CreateGameViewModel import dev.lucasnlm.antimine.custom.viewmodel.CreateGameViewModel
import dev.lucasnlm.antimine.custom.viewmodel.CustomEvent import dev.lucasnlm.antimine.custom.viewmodel.CustomEvent
import javax.inject.Inject import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class CustomLevelDialogFragment : AppCompatDialogFragment() { class CustomLevelDialogFragment : AppCompatDialogFragment() {
@Inject private val gameViewModel by sharedViewModel<GameViewModel>()
lateinit var preferencesRepository: IPreferencesRepository private val createGameViewModel by viewModel<CreateGameViewModel>()
private val viewModel by activityViewModels<GameViewModel>() private lateinit var mapWidth: TextView
private val createGameViewModel by activityViewModels<CreateGameViewModel>() private lateinit var mapHeight: TextView
private lateinit var mapMines: TextView
private fun getSelectedMinefield(): Minefield { private fun getSelectedMinefield(): Minefield {
val mapWidth: TextView? = dialog?.findViewById(R.id.map_width) val width = filterInput(mapWidth.text.toString(), MIN_WIDTH).coerceAtMost(MAX_WIDTH)
val mapHeight: TextView? = dialog?.findViewById(R.id.map_height) val height = filterInput(mapHeight.text.toString(), MIN_HEIGHT).coerceAtMost(MAX_HEIGHT)
val mapMines: TextView? = dialog?.findViewById(R.id.map_mines) val mines = filterInput(mapMines.text.toString(), MIN_MINES).coerceAtMost(width * height - 1)
val width = filterInput(
mapWidth?.text.toString(),
MIN_WIDTH
).coerceAtMost(MAX_WIDTH)
val height = filterInput(
mapHeight?.text.toString(),
MIN_HEIGHT
).coerceAtMost(MAX_HEIGHT)
val mines = filterInput(
mapMines?.text.toString(),
MIN_MINES
).coerceAtMost(width * height - 1)
return Minefield(width, height, mines) return Minefield(width, height, mines)
} }
@SuppressLint("InflateParams")
private fun createView(): View {
val view = LayoutInflater
.from(context)
.inflate(R.layout.dialog_custom_game, null, false)
mapWidth = view.findViewById(R.id.map_width)
mapHeight = view.findViewById(R.id.map_height)
mapMines = view.findViewById(R.id.map_mines)
createGameViewModel.singleState().let {
mapWidth.text = it.width.toString()
mapHeight.text = it.height.toString()
mapMines.text = it.mines.toString()
}
return view
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply { return AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.new_game) setTitle(R.string.new_game)
setView(R.layout.dialog_custom_game) setView(createView())
setNegativeButton(R.string.cancel, null) setNegativeButton(R.string.cancel, null)
setPositiveButton(R.string.start) { _, _ -> setPositiveButton(R.string.start) { _, _ ->
val minefield = getSelectedMinefield() val minefield = getSelectedMinefield()
createGameViewModel.sendEvent(CustomEvent.UpdateCustomGameEvent(minefield)) createGameViewModel.sendEvent(CustomEvent.UpdateCustomGameEvent(minefield))
viewModel.startNewGame(Difficulty.Custom) gameViewModel.startNewGame(Difficulty.Custom)
} }
}.create() }.create()
} }

View file

@ -1,17 +1,22 @@
package dev.lucasnlm.antimine.custom.viewmodel package dev.lucasnlm.antimine.custom.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.viewmodel.StatelessViewModel import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow
class CreateGameViewModel @ViewModelInject constructor( class CreateGameViewModel(
private val preferencesRepository: IPreferencesRepository private val preferencesRepository: IPreferencesRepository
) : StatelessViewModel<CustomEvent>() { ) : IntentViewModel<CustomEvent, CustomState>() {
override fun onEvent(event: CustomEvent) {
when (event) { override suspend fun mapEventToState(event: CustomEvent) = flow {
is CustomEvent.UpdateCustomGameEvent -> { if (event is CustomEvent.UpdateCustomGameEvent) {
preferencesRepository.updateCustomGameMode(event.minefield) val minefield = event.minefield
} preferencesRepository.updateCustomGameMode(minefield)
emit(CustomState(minefield.width, minefield.height, minefield.mines))
} }
} }
override fun initialState() = with(preferencesRepository.customGameMode()) {
CustomState(width, height, mines)
}
} }

View file

@ -0,0 +1,7 @@
package dev.lucasnlm.antimine.custom.viewmodel
data class CustomState(
val width: Int,
val height: Int,
val mines: Int
)

View file

@ -1,18 +1,34 @@
package dev.lucasnlm.antimine.di package dev.lucasnlm.antimine.di
import android.content.Context import dev.lucasnlm.antimine.common.BuildConfig
import dagger.Module import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager
import dagger.Provides import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dagger.hilt.InstallIn import dev.lucasnlm.antimine.core.analytics.ProdAnalyticsManager
import dagger.hilt.android.components.ApplicationComponent import dev.lucasnlm.antimine.share.ShareManager
import dagger.hilt.android.qualifiers.ApplicationContext import dev.lucasnlm.external.BillingManager
import dev.lucasnlm.antimine.instant.InstantAppManager import dev.lucasnlm.external.ExternalAnalyticsWrapper
import dev.lucasnlm.external.IBillingManager
import dev.lucasnlm.external.IInstantAppManager
import dev.lucasnlm.external.IPlayGamesManager
import dev.lucasnlm.external.InstantAppManager
import dev.lucasnlm.external.PlayGamesManager
import org.koin.dsl.bind
import org.koin.dsl.module
@Module val AppModule = module {
@InstallIn(ApplicationComponent::class) single { InstantAppManager() } bind IInstantAppManager::class
class AppModule {
@Provides single { BillingManager(get()) } bind IBillingManager::class
fun provideInstantAppManager(
@ApplicationContext context: Context single { PlayGamesManager(get()) } bind IPlayGamesManager::class
): InstantAppManager = InstantAppManager(context)
single { ShareManager(get()) }
single {
if (BuildConfig.DEBUG) {
DebugAnalyticsManager()
} else {
ProdAnalyticsManager(ExternalAnalyticsWrapper(get()))
}
} bind IAnalyticsManager::class
} }

View file

@ -0,0 +1,29 @@
package dev.lucasnlm.antimine.di
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel
import dev.lucasnlm.antimine.custom.viewmodel.CreateGameViewModel
import dev.lucasnlm.antimine.history.viewmodel.HistoryViewModel
import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogViewModel
import dev.lucasnlm.antimine.playgames.viewmodel.PlayGamesViewModel
import dev.lucasnlm.antimine.stats.viewmodel.StatsViewModel
import dev.lucasnlm.antimine.text.viewmodel.TextViewModel
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val ViewModelModule = module {
viewModel { AboutViewModel(get()) }
viewModel { ControlViewModel(get()) }
viewModel { CreateGameViewModel(get()) }
viewModel { HistoryViewModel(get(), get()) }
viewModel { EndGameDialogViewModel(get()) }
viewModel { PlayGamesViewModel(get(), get()) }
viewModel { StatsViewModel(get(), get()) }
viewModel { TextViewModel(get()) }
viewModel { ThemeViewModel(get()) }
viewModel {
GameViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}
}

View file

@ -1,12 +1,10 @@
package dev.lucasnlm.antimine.history package dev.lucasnlm.antimine.history
import android.os.Bundle import android.os.Bundle
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.history.views.HistoryFragment import dev.lucasnlm.antimine.history.views.HistoryFragment
@AndroidEntryPoint
class HistoryActivity : ThematicActivity(R.layout.activity_empty) { class HistoryActivity : ThematicActivity(R.layout.activity_empty) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -3,15 +3,13 @@ package dev.lucasnlm.antimine.history.viewmodel
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.hilt.lifecycle.ViewModelInject
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.lucasnlm.antimine.DeepLink import dev.lucasnlm.antimine.DeepLink
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
class HistoryViewModel @ViewModelInject constructor( class HistoryViewModel(
@ApplicationContext private val context: Context, private val context: Context,
private val savesRepository: ISavesRepository private val savesRepository: ISavesRepository
) : IntentViewModel<HistoryEvent, HistoryState>() { ) : IntentViewModel<HistoryEvent, HistoryState>() {
@ -31,7 +29,7 @@ class HistoryViewModel @ViewModelInject constructor(
} }
} }
override suspend fun mapEventToState(event: HistoryEvent) = flow<HistoryState> { override suspend fun mapEventToState(event: HistoryEvent) = flow {
when (event) { when (event) {
is HistoryEvent.LoadAllSaves -> { is HistoryEvent.LoadAllSaves -> {
val newSaveList = savesRepository.getAllSaves().sortedByDescending { it.uid } val newSaveList = savesRepository.getAllSaves().sortedByDescending { it.uid }

View file

@ -3,20 +3,18 @@ package dev.lucasnlm.antimine.history.views
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.history.viewmodel.HistoryEvent import dev.lucasnlm.antimine.history.viewmodel.HistoryEvent
import dev.lucasnlm.antimine.history.viewmodel.HistoryViewModel import dev.lucasnlm.antimine.history.viewmodel.HistoryViewModel
import kotlinx.android.synthetic.main.fragment_history.* import kotlinx.android.synthetic.main.fragment_history.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) { class HistoryFragment : Fragment(R.layout.fragment_history) {
private val historyViewModel: HistoryViewModel by activityViewModels() private val historyViewModel by viewModel<HistoryViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View file

@ -1,17 +0,0 @@
package dev.lucasnlm.antimine.instant
import android.app.Activity
import android.content.Context
import android.content.Intent
import dev.lucasnlm.external.InstantAppWrapper
class InstantAppManager(
private val context: Context
) {
fun isEnabled(): Boolean = InstantAppWrapper().isEnabled(context)
fun isNotEnabled(): Boolean = isEnabled().not()
fun showInstallPrompt(activity: Activity, intent: Intent?, requestCode: Int, referrer: String?) =
InstantAppWrapper().showInstallPrompt(activity, intent, requestCode, referrer)
}

View file

@ -9,24 +9,22 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.instant.InstantAppManager
import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogEvent import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogEvent
import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogViewModel import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogViewModel
import dev.lucasnlm.external.IInstantAppManager
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import javax.inject.Inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class EndGameDialogFragment : AppCompatDialogFragment() { class EndGameDialogFragment : AppCompatDialogFragment() {
@Inject private val instantAppManager: IInstantAppManager by inject()
lateinit var instantAppManager: InstantAppManager
private val endGameViewModel by activityViewModels<EndGameDialogViewModel>() private val endGameViewModel by viewModel<EndGameDialogViewModel>()
private val viewModel by activityViewModels<GameViewModel>() private val gameViewModel by sharedViewModel<GameViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -70,7 +68,7 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
} }
when { when {
instantAppManager.isEnabled() -> { instantAppManager.isEnabled(context) -> {
setNeutralButton(R.string.install) { _, _ -> setNeutralButton(R.string.install) { _, _ ->
activity?.run { activity?.run {
instantAppManager.showInstallPrompt(this, null, 0, null) instantAppManager.showInstallPrompt(this, null, 0, null)
@ -79,12 +77,12 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
} }
state.isVictory == true -> { state.isVictory == true -> {
setNeutralButton(R.string.share) { _, _ -> setNeutralButton(R.string.share) { _, _ ->
viewModel.shareObserver.postValue(Unit) gameViewModel.shareObserver.postValue(Unit)
} }
} }
else -> { else -> {
setNeutralButton(R.string.retry) { _, _ -> setNeutralButton(R.string.retry) { _, _ ->
viewModel.retryObserver.postValue(Unit) gameViewModel.retryObserver.postValue(Unit)
} }
} }
} }
@ -95,7 +93,7 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
setView(view) setView(view)
setPositiveButton(R.string.new_game) { _, _ -> setPositiveButton(R.string.new_game) { _, _ ->
viewModel.startNewGame() gameViewModel.startNewGame()
} }
}.create() }.create()

View file

@ -1,39 +1,57 @@
package dev.lucasnlm.antimine.level.view package dev.lucasnlm.antimine.level.view
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.View import android.view.View
import androidx.core.view.doOnLayout
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.DeepLink import dev.lucasnlm.antimine.DeepLink
import dev.lucasnlm.antimine.common.R import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.common.level.models.Difficulty import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Event import dev.lucasnlm.antimine.common.level.models.Event
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.view.CommonLevelFragment import dev.lucasnlm.antimine.common.level.view.CommonLevelFragment
import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@AndroidEntryPoint
open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) { open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
GlobalScope.launch { lifecycleScope.launch {
viewModel.saveGame() gameViewModel.saveGame()
} }
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
recyclerGrid = view.findViewById(R.id.recyclerGrid) recyclerGrid = view.findViewById(R.id.recyclerGrid)
recyclerGrid.doOnLayout {
lifecycleScope.launch {
val loadGameUid = checkLoadGameDeepLink()
val newGameDeepLink = checkNewGameDeepLink()
val retryDeepLink = checkRetryGameDeepLink()
viewModel.run { val levelSetup = when {
loadGameUid != null -> gameViewModel.loadGame(loadGameUid)
newGameDeepLink != null -> gameViewModel.startNewGame(newGameDeepLink)
retryDeepLink != null -> gameViewModel.retryGame(retryDeepLink)
else -> gameViewModel.loadLastGame()
}
withContext(Dispatchers.Main) {
recyclerGrid.apply {
addItemDecoration(SpaceItemDecoration(R.dimen.field_padding))
setHasFixedSize(true)
}
setupRecyclerViewSize(levelSetup)
}
}
}
gameViewModel.run {
field.observe( field.observe(
viewLifecycleOwner, viewLifecycleOwner,
Observer { Observer {
@ -44,20 +62,7 @@ open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) {
levelSetup.observe( levelSetup.observe(
viewLifecycleOwner, viewLifecycleOwner,
Observer { Observer {
recyclerGrid.apply { setupRecyclerViewSize(it)
val horizontalPadding = calcHorizontalPadding(it.width)
val verticalPadding = calcVerticalPadding(it.height)
layoutManager = makeNewLayoutManager(it.width)
setHasFixedSize(true)
setPadding(horizontalPadding, verticalPadding, 0, 0)
}
}
)
fieldRefresh.observe(
viewLifecycleOwner,
Observer {
areaAdapter.notifyItemChanged(it)
} }
) )
@ -81,38 +86,19 @@ open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) {
} }
} }
override fun onAttach(context: Context) { private fun setupRecyclerViewSize(levelSetup: Minefield) {
super.onAttach(context) recyclerGrid.apply {
val horizontalPadding = calcHorizontalPadding(levelSetup.width)
val verticalPadding = calcVerticalPadding(levelSetup.height)
setPadding(horizontalPadding, verticalPadding, 0, 0)
layoutManager = makeNewLayoutManager(levelSetup.width)
adapter = areaAdapter
alpha = 0.0f
lifecycleScope.launchWhenCreated { animate().apply {
val loadGameUid = checkLoadGameDeepLink() alpha(1.0f)
val newGameDeepLink = checkNewGameDeepLink() duration = DateUtils.SECOND_IN_MILLIS
val retryDeepLink = checkRetryGameDeepLink() }.start()
val levelSetup = when {
loadGameUid != null -> viewModel.loadGame(loadGameUid)
newGameDeepLink != null -> viewModel.startNewGame(newGameDeepLink)
retryDeepLink != null -> viewModel.retryGame(retryDeepLink)
else -> viewModel.loadLastGame()
}
withContext(Dispatchers.Main) {
recyclerGrid.apply {
val horizontalPadding = calcHorizontalPadding(levelSetup.width)
val verticalPadding = calcVerticalPadding(levelSetup.height)
addItemDecoration(SpaceItemDecoration(R.dimen.field_padding))
setPadding(horizontalPadding, verticalPadding, 0, 0)
layoutManager = makeNewLayoutManager(levelSetup.width)
setHasFixedSize(true)
adapter = areaAdapter
alpha = 0.0f
animate().apply {
alpha(1.0f)
duration = DateUtils.SECOND_IN_MILLIS
}.start()
}
}
} }
} }

View file

@ -1,14 +1,12 @@
package dev.lucasnlm.antimine.level.viewmodel package dev.lucasnlm.antimine.level.viewmodel
import android.content.Context import android.content.Context
import androidx.hilt.lifecycle.ViewModelInject
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
class EndGameDialogViewModel @ViewModelInject constructor( class EndGameDialogViewModel(
@ApplicationContext private val context: Context private val context: Context
) : IntentViewModel<EndGameDialogEvent, EndGameDialogState>() { ) : IntentViewModel<EndGameDialogEvent, EndGameDialogState>() {
private fun List<Int>.safeRandomEmoji( private fun List<Int>.safeRandomEmoji(
@ -28,6 +26,7 @@ class EndGameDialogViewModel @ViewModelInject constructor(
R.drawable.emoji_grinning_squinting_face, R.drawable.emoji_grinning_squinting_face,
R.drawable.emoji_smiling_face_with_sunglasses, R.drawable.emoji_smiling_face_with_sunglasses,
R.drawable.emoji_squinting_face_with_tongue, R.drawable.emoji_squinting_face_with_tongue,
R.drawable.emoji_hugging_face,
R.drawable.emoji_partying_face, R.drawable.emoji_partying_face,
R.drawable.emoji_clapping_hands, R.drawable.emoji_clapping_hands,
R.drawable.emoji_triangular_flag R.drawable.emoji_triangular_flag
@ -74,7 +73,7 @@ class EndGameDialogViewModel @ViewModelInject constructor(
false false
) )
override suspend fun mapEventToState(event: EndGameDialogEvent) = flow<EndGameDialogState> { override suspend fun mapEventToState(event: EndGameDialogEvent) = flow {
if (event is EndGameDialogEvent.BuildCustomEndGame) { if (event is EndGameDialogEvent.BuildCustomEndGame) {
val state = when (event.isVictory) { val state = when (event.isVictory) {
true -> { true -> {

View file

@ -0,0 +1,104 @@
package dev.lucasnlm.antimine.playgames
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.playgames.viewmodel.PlayGamesEvent
import dev.lucasnlm.antimine.playgames.viewmodel.PlayGamesViewModel
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
class PlayGamesDialogFragment : DialogFragment() {
private val playGamesViewModel by viewModel<PlayGamesViewModel>()
private val adapter by lazy { PlayGamesAdapter(playGamesViewModel) }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply {
setTitle(R.string.google_play_games)
setAdapter(adapter, null)
setPositiveButton(R.string.ok, null)
}.create()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated {
playGamesViewModel.observeEvent().collect {
when (it) {
is PlayGamesEvent.OpenAchievements -> {
activity?.let { activity ->
playGamesViewModel.openAchievements(activity)
}
}
is PlayGamesEvent.OpenLeaderboards -> {
activity?.let { activity ->
playGamesViewModel.openLeaderboards(activity)
}
}
}
}
}
}
private class PlayGamesAdapter(
private val playGamesViewModel: PlayGamesViewModel
) : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = if (convertView == null) {
PlayGamesButton(parent!!.context)
} else {
(convertView as PlayGamesButton)
}
val item = playGamesViewModel.playGamesItems[position]
return view.apply {
text.text = view.context.getString(item.stringRes)
icon.setImageResource(item.iconRes)
setOnClickListener {
playGamesViewModel.sendEvent(item.triggerEvent)
}
}
}
override fun hasStableIds(): Boolean = true
override fun getItem(position: Int): Any = playGamesViewModel.playGamesItems[position]
override fun getItemId(position: Int): Long = playGamesViewModel.playGamesItems[position].id.toLong()
override fun getCount(): Int = playGamesViewModel.playGamesItems.count()
}
companion object {
val TAG = PlayGamesDialogFragment::class.simpleName
}
}
class PlayGamesButton : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
inflate(context, R.layout.view_play_games_button, this)
icon = findViewById(R.id.icon)
text = findViewById(R.id.text)
}
val icon: AppCompatImageView
val text: TextView
}

View file

@ -0,0 +1,12 @@
package dev.lucasnlm.antimine.playgames.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import dev.lucasnlm.antimine.playgames.viewmodel.PlayGamesEvent
data class PlayGamesItem(
val id: Int,
@DrawableRes val iconRes: Int,
@StringRes val stringRes: Int,
val triggerEvent: PlayGamesEvent
)

View file

@ -0,0 +1,6 @@
package dev.lucasnlm.antimine.playgames.viewmodel
sealed class PlayGamesEvent {
object OpenAchievements : PlayGamesEvent()
object OpenLeaderboards : PlayGamesEvent()
}

View file

@ -0,0 +1,27 @@
package dev.lucasnlm.antimine.playgames.viewmodel
import android.app.Activity
import android.content.Context
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.core.viewmodel.StatelessViewModel
import dev.lucasnlm.antimine.playgames.model.PlayGamesItem
import dev.lucasnlm.external.IPlayGamesManager
class PlayGamesViewModel(
private val context: Context,
private val playGamesManager: IPlayGamesManager
) : StatelessViewModel<PlayGamesEvent>() {
val playGamesItems = listOf(
PlayGamesItem(0, R.drawable.games_achievements, R.string.achievements, PlayGamesEvent.OpenAchievements),
PlayGamesItem(1, R.drawable.games_leaderboards, R.string.leaderboards, PlayGamesEvent.OpenLeaderboards)
)
fun openAchievements(activity: Activity) {
playGamesManager.openAchievements(activity)
}
fun openLeaderboards(activity: Activity) {
playGamesManager.openLeaderboards(activity)
}
}

View file

@ -3,11 +3,9 @@ package dev.lucasnlm.antimine.preferences
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity import dev.lucasnlm.antimine.ThematicActivity
@AndroidEntryPoint
class PreferencesActivity : ThematicActivity(R.layout.activity_empty) { class PreferencesActivity : ThematicActivity(R.layout.activity_empty) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View file

@ -9,6 +9,7 @@ import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.graphics.Typeface import android.graphics.Typeface
import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import dev.lucasnlm.antimine.BuildConfig import dev.lucasnlm.antimine.BuildConfig
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
@ -22,12 +23,10 @@ import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
class ShareBuilder( class ShareManager(
context: Context private val context: Context
) { ) {
private val context: Context = context.applicationContext private suspend fun share(minefield: Minefield, field: List<Area>): Boolean {
suspend fun share(minefield: Minefield, field: List<Area>): Boolean {
val file = createImage(minefield, field) val file = createImage(minefield, field)
return if (file != null) { return if (file != null) {
@ -129,4 +128,16 @@ class ShareBuilder(
false false
} }
} }
suspend fun shareField(minefield: Minefield?, field: List<Area>?) {
val result = if (minefield != null && field != null && field.count() != 0) {
share(minefield, field)
} else {
false
}
if (!result) {
Toast.makeText(context, context.getString(R.string.fail_to_share), Toast.LENGTH_SHORT).show()
}
}
} }

View file

@ -1,27 +0,0 @@
package dev.lucasnlm.antimine.share.viewmodel
import android.app.Application
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.share.ShareBuilder
import dev.lucasnlm.antimine.common.level.models.Area
import dev.lucasnlm.antimine.common.level.models.Minefield
class ShareViewModel(
application: Application
) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
suspend fun share(minefield: Minefield?, field: List<Area>?) {
val result = if (minefield != null && field != null && field.count() != 0) {
ShareBuilder(context).share(minefield, field)
} else {
false
}
if (!result) {
Toast.makeText(context, context.getString(R.string.fail_to_share), Toast.LENGTH_SHORT).show()
}
}
}

View file

@ -3,10 +3,8 @@ package dev.lucasnlm.antimine.stats
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.stats.model.StatsModel import dev.lucasnlm.antimine.stats.model.StatsModel
@ -16,19 +14,19 @@ import kotlinx.android.synthetic.main.activity_stats.*
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class StatsActivity : ThematicActivity(R.layout.activity_stats) { class StatsActivity : ThematicActivity(R.layout.activity_stats) {
private val viewModel: StatsViewModel by viewModels() private val statsViewModel by viewModel<StatsViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
refreshStats(StatsViewModel.emptyStats) refreshStats(StatsViewModel.emptyStats)
lifecycleScope.launchWhenResumed { lifecycleScope.launchWhenResumed {
viewModel.sendEvent(StatsEvent.LoadStats) statsViewModel.sendEvent(StatsEvent.LoadStats)
viewModel.observeState().collect { statsViewModel.observeState().collect {
refreshStats(it) refreshStats(it)
} }
} }
@ -61,7 +59,7 @@ class StatsActivity : ThematicActivity(R.layout.activity_stats) {
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
viewModel.singleState().let { statsViewModel.singleState().let {
if (it.totalGames > 0) { if (it.totalGames > 0) {
menuInflater.inflate(R.menu.stats_menu, menu) menuInflater.inflate(R.menu.stats_menu, menu)
} }
@ -85,7 +83,7 @@ class StatsActivity : ThematicActivity(R.layout.activity_stats) {
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete_all) { _, _ -> .setPositiveButton(R.string.delete_all) { _, _ ->
GlobalScope.launch { GlobalScope.launch {
viewModel.sendEvent(StatsEvent.DeleteStats) statsViewModel.sendEvent(StatsEvent.DeleteStats)
} }
} }
.show() .show()

View file

@ -1,13 +1,12 @@
package dev.lucasnlm.antimine.stats.viewmodel package dev.lucasnlm.antimine.stats.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import dev.lucasnlm.antimine.stats.model.StatsModel import dev.lucasnlm.antimine.stats.model.StatsModel
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
class StatsViewModel @ViewModelInject constructor( class StatsViewModel(
private val statsRepository: IStatsRepository, private val statsRepository: IStatsRepository,
private val preferenceRepository: IPreferencesRepository private val preferenceRepository: IPreferencesRepository
) : IntentViewModel<StatsEvent, StatsModel>() { ) : IntentViewModel<StatsEvent, StatsModel>() {

View file

@ -0,0 +1,37 @@
package dev.lucasnlm.antimine.support
import android.app.Dialog
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.lifecycle.lifecycleScope
import dev.lucasnlm.antimine.R
import dev.lucasnlm.external.IBillingManager
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
class SupportAppDialogFragment : AppCompatDialogFragment() {
private val billingManager: IBillingManager by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
billingManager.start()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply {
setView(R.layout.dialog_payments)
setNeutralButton(R.string.rating_button_no, null)
setPositiveButton(R.string.unlock) { _, _ ->
lifecycleScope.launch {
billingManager.charge(requireActivity())
}
}
}.create()
}
companion object {
val TAG = SupportAppDialogFragment::class.simpleName
}
}

View file

@ -4,10 +4,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.viewModels
import androidx.annotation.RawRes import androidx.annotation.RawRes
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.text.viewmodel.TextEvent import dev.lucasnlm.antimine.text.viewmodel.TextEvent
@ -16,10 +14,10 @@ import kotlinx.android.synthetic.main.activity_text.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class TextActivity : ThematicActivity(R.layout.activity_text) { class TextActivity : ThematicActivity(R.layout.activity_text) {
private val viewModel: TextViewModel by viewModels() private val textViewModel by viewModel<TextViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -28,7 +26,7 @@ class TextActivity : ThematicActivity(R.layout.activity_text) {
title = bundle.getString(TEXT_TITLE) title = bundle.getString(TEXT_TITLE)
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
viewModel.sendEvent( textViewModel.sendEvent(
TextEvent.LoadText( TextEvent.LoadText(
title = bundle.getString(TEXT_TITLE, ""), title = bundle.getString(TEXT_TITLE, ""),
rawFileRes = bundle.getInt(TEXT_PATH, -1) rawFileRes = bundle.getInt(TEXT_PATH, -1)
@ -39,7 +37,7 @@ class TextActivity : ThematicActivity(R.layout.activity_text) {
progressBar.visibility = View.VISIBLE progressBar.visibility = View.VISIBLE
} }
viewModel.observeState().collect { textViewModel.observeState().collect {
textView.text = it.body textView.text = it.body
progressBar.visibility = View.GONE progressBar.visibility = View.GONE
} }

View file

@ -2,16 +2,14 @@ package dev.lucasnlm.antimine.text.viewmodel
import android.content.Context import android.content.Context
import androidx.annotation.RawRes import androidx.annotation.RawRes
import androidx.hilt.lifecycle.ViewModelInject
import dagger.hilt.android.qualifiers.ApplicationContext
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import dev.lucasnlm.antimine.text.models.TextState import dev.lucasnlm.antimine.text.models.TextState
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class TextViewModel @ViewModelInject constructor( class TextViewModel(
@ApplicationContext private val context: Context private val context: Context
) : IntentViewModel<TextEvent, TextState>() { ) : IntentViewModel<TextEvent, TextState>() {
private suspend fun loadText(@RawRes rawFile: Int): String { private suspend fun loadText(@RawRes rawFile: Int): String {

View file

@ -1,26 +1,24 @@
package dev.lucasnlm.antimine.theme package dev.lucasnlm.antimine.theme
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import dev.lucasnlm.antimine.R import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration import dev.lucasnlm.antimine.common.level.view.SpaceItemDecoration
import dev.lucasnlm.antimine.support.SupportAppDialogFragment
import dev.lucasnlm.antimine.theme.view.ThemeAdapter import dev.lucasnlm.antimine.theme.view.ThemeAdapter
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import kotlinx.android.synthetic.main.activity_theme.* import kotlinx.android.synthetic.main.activity_theme.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import javax.inject.Inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@AndroidEntryPoint
class ThemeActivity : ThematicActivity(R.layout.activity_theme) { class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
@Inject private val dimensionRepository: IDimensionRepository by inject()
lateinit var dimensionRepository: IDimensionRepository
private val viewModel by viewModels<ThemeViewModel>() private val themeViewModel by viewModel<ThemeViewModel>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -32,14 +30,22 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
addItemDecoration(SpaceItemDecoration(R.dimen.theme_divider)) addItemDecoration(SpaceItemDecoration(R.dimen.theme_divider))
setHasFixedSize(true) setHasFixedSize(true)
layoutManager = GridLayoutManager(context, 3) layoutManager = GridLayoutManager(context, 3)
adapter = ThemeAdapter(viewModel, areaSize) adapter = ThemeAdapter(themeViewModel, areaSize)
} }
viewModel.observeState().collect { themeViewModel.observeState().collect {
if (usingTheme.id != it.current.id) { if (usingTheme.id != it.current.id) {
recreate() recreate()
} }
} }
} }
} }
private fun showUnlockDialog() {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment().apply {
show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}
}
} }

View file

@ -58,14 +58,27 @@ class ThemeAdapter(
val theme = themes[position] val theme = themes[position]
val paintSettings = createAreaPaintSettings(holder.itemView.context, areaSize) val paintSettings = createAreaPaintSettings(holder.itemView.context, areaSize)
holder.itemView.run { holder.itemView.run {
val selected = (theme.id == themeViewModel.singleState().current.id)
val areas = listOf(area0, area1, area2, area3, area4, area5, area6, area7, area8) val areas = listOf(area0, area1, area2, area3, area4, area5, area6, area7, area8)
if (selected) {
areas.forEach { it.alpha = 0.25f }
} else {
areas.forEach { it.alpha = 1.0f }
}
areas.forEachIndexed { index, areaView -> areaView.bindTheme(minefield[index], theme, paintSettings) } areas.forEachIndexed { index, areaView -> areaView.bindTheme(minefield[index], theme, paintSettings) }
if (position == 0) { if (position < 2 && !selected) {
areas.forEach { it.alpha = 0.35f } areas.forEach { it.alpha = 0.35f }
label.apply { label.apply {
text = if (position == 0) {
label.context.getString(R.string.system)
} else {
label.context.getString(R.string.amoled)
}
setTextColor( setTextColor(
with(theme.palette.background) { with(theme.palette.background) {
Color.rgb(255 - Color.red(this), 255 - Color.green(this), 255 - Color.blue(this)) Color.rgb(255 - Color.red(this), 255 - Color.green(this), 255 - Color.blue(this))

View file

@ -1,19 +1,18 @@
package dev.lucasnlm.antimine.theme.viewmodel package dev.lucasnlm.antimine.theme.viewmodel
import androidx.hilt.lifecycle.ViewModelInject
import dev.lucasnlm.antimine.core.themes.model.AppTheme import dev.lucasnlm.antimine.core.themes.model.AppTheme
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
class ThemeViewModel @ViewModelInject constructor( class ThemeViewModel(
private val themeRepository: IThemeRepository private val themeRepository: IThemeRepository
) : IntentViewModel<ThemeEvent, ThemeState>() { ) : IntentViewModel<ThemeEvent, ThemeState>() {
private fun setTheme(theme: AppTheme) { private fun setTheme(theme: AppTheme) {
themeRepository.setTheme(theme) themeRepository.setTheme(theme)
} }
override suspend fun mapEventToState(event: ThemeEvent) = flow<ThemeState> { override suspend fun mapEventToState(event: ThemeEvent) = flow {
if (event is ThemeEvent.ChangeTheme) { if (event is ThemeEvent.ChangeTheme) {
setTheme(event.newTheme) setTheme(event.newTheme)
emit(state.copy(current = event.newTheme)) emit(state.copy(current = event.newTheme))

View file

@ -0,0 +1,359 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="M63.6,118.8c-27.9,0 -58,-17.5 -58,-55.9S35.7,7 63.6,7c15.5,0 29.8,5.1 40.4,14.4c11.5,10.2 17.6,24.6 17.6,41.5s-6.1,31.2 -17.6,41.4c-10.6,9.3 -25,14.5 -40.4,14.5z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="56.96"
android:centerX="63.6"
android:centerY="62.9"
android:type="radial">
<item android:offset="0.5" android:color="#FFFDE030"/>
<item android:offset="0.919" android:color="#FFF7C02B"/>
<item android:offset="1" android:color="#FFF4A223"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M63.6,118.8c-27.9,0 -58,-17.5 -58,-55.9S35.7,7 63.6,7c15.5,0 29.8,5.1 40.4,14.4c11.5,10.2 17.6,24.6 17.6,41.5s-6.1,31.2 -17.6,41.4c-10.6,9.3 -25,14.5 -40.4,14.5z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="118.8"
android:startX="63.6"
android:endY="7"
android:endX="63.6"
android:type="linear">
<item android:offset="0.158" android:color="#FFF4A223"/>
<item android:offset="0.333" android:color="#FFF7C02B"/>
<item android:offset="0.807" android:color="#00FDE030"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M111.49,29.67c5.33,8.6 8.11,18.84 8.11,30.23c0,16.9 -6.1,31.2 -17.6,41.4c-10.6,9.3 -25,14.5 -40.4,14.5c-18.06,0 -37.04,-7.35 -48.18,-22.94c10.76,17.66 30.99,25.94 50.18,25.94c15.4,0 29.8,-5.2 40.4,-14.5c11.5,-10.2 17.6,-24.5 17.6,-41.4c0,-12.74 -3.47,-24.06 -10.11,-33.23z"
android:fillColor="#eb8f00"/>
<path
android:pathData="M29.7,64.1m-16.3,0a16.3,16.3 0,1 1,32.6 0a16.3,16.3 0,1 1,-32.6 0"
android:strokeAlpha="0.8"
android:fillAlpha="0.8">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="17.73736"
android:centerX="29.6654"
android:centerY="64.10675"
android:type="radial">
<item android:offset="0" android:color="#FFED7770"/>
<item android:offset="0.9" android:color="#00ED7770"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M96.9,51.5m-16.3,0a16.3,16.3 0,1 1,32.6 0a16.3,16.3 0,1 1,-32.6 0"
android:strokeAlpha="0.8"
android:fillAlpha="0.8">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="17.735403"
android:centerX="96.90706"
android:centerY="51.534607"
android:type="radial">
<item android:offset="0" android:color="#FFED7770"/>
<item android:offset="0.9" android:color="#00ED7770"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M96.9,49.4C89.2,56.5 79,62.1 67.2,64.8c-11.8,2.7 -23.4,2.2 -33.4,-0.9c-1.8,-0.6 -3.1,1.8 -1.5,3c10.1,7.5 23.6,10.7 37.1,7.5c13.5,-3.1 24.3,-11.8 30.1,-23c1,-1.7 -1.2,-3.3 -2.6,-2z"
android:fillColor="#422b0d"/>
<path
android:pathData="M48.5,47.2l-0.2,-0.2c-0.1,-0.1 -0.3,-0.2 -0.5,-0.4c-0.2,-0.1 -0.4,-0.3 -0.6,-0.4c-0.2,-0.2 -0.5,-0.4 -0.8,-0.6c-0.3,-0.2 -0.6,-0.4 -0.9,-0.5c-0.3,-0.2 -0.6,-0.3 -0.9,-0.3c-0.3,-0.1 -0.5,-0.1 -0.6,-0.1h-0.2h-0.1h0.2l-0.5,0.1c-0.1,0 0,0 0,0h0.1c0.1,0 0,0 0,0h-0.1c-0.1,0.1 -0.3,0.2 -0.5,0.3c-0.2,0.2 -0.5,0.4 -0.7,0.6c-0.2,0.2 -0.4,0.5 -0.6,0.8c-0.4,0.6 -0.7,1.1 -0.9,1.5s-0.4,0.6 -0.4,0.6l-0.2,0.4c-1,1.7 -3.2,2.4 -5,1.5c-1.2,-0.6 -1.9,-1.7 -2,-2.9v-1.1c0.1,-0.7 0.2,-1.7 0.6,-2.9s1.1,-2.7 2.4,-4.1c0.7,-0.7 1.5,-1.5 2.5,-2c0.2,-0.2 0.5,-0.3 0.8,-0.4c0.3,-0.1 0.5,-0.3 0.9,-0.4l0.5,-0.2c0.2,-0.1 0.4,-0.1 0.5,-0.1l0.5,-0.1l0.3,-0.1h0.4l0.5,-0.1h0.9c0.6,0 1.2,0 1.8,0.1c1.2,0.2 2.2,0.6 3.1,1c1.8,0.9 2.9,2 3.8,2.9c0.4,0.5 0.8,0.9 1,1.3c0.3,0.4 0.5,0.8 0.7,1.1s0.2,0.5 0.3,0.6c0,0.1 0.1,0.2 0.1,0.2c0.7,1.8 -0.3,3.8 -2.3,4.5c-1.4,0.5 -2.9,0.2 -3.9,-0.6z"
android:fillColor="#422b0d"/>
<path
android:pathData="M82.8,40.5l-0.2,-0.2c-0.1,-0.1 -0.3,-0.2 -0.5,-0.4c-0.2,-0.1 -0.4,-0.3 -0.6,-0.4c-0.2,-0.2 -0.5,-0.4 -0.8,-0.6c-0.3,-0.2 -0.6,-0.4 -0.9,-0.5c-0.3,-0.2 -0.6,-0.3 -0.9,-0.3c-0.2,-0.1 -0.4,-0.1 -0.5,-0.1H78.1h0.2l-0.5,0.1c-0.1,0 0,0 0,0h0.1c0.1,0 0,0 0,0h-0.1c-0.1,0.1 -0.3,0.2 -0.6,0.3c-0.2,0.2 -0.4,0.4 -0.7,0.6c-0.2,0.2 -0.4,0.5 -0.6,0.8c-0.4,0.6 -0.7,1.1 -0.9,1.5c-0.2,0.4 -0.4,0.6 -0.4,0.6l-0.2,0.3c-1,1.7 -3.2,2.4 -5,1.5c-1.2,-0.6 -1.9,-1.7 -2,-2.9c0,0 0,-0.4 0.1,-1.1c0.1,-0.7 0.2,-1.7 0.6,-2.9s1.1,-2.7 2.4,-4.1c0.7,-0.7 1.5,-1.5 2.5,-2c0.2,-0.1 0.5,-0.3 0.8,-0.4s0.5,-0.3 0.9,-0.4l0.6,-0.2c0.2,-0.1 0.4,-0.1 0.5,-0.1l0.5,-0.1l0.3,-0.1h0.4l0.5,-0.1h0.9c0.6,0 1.2,0 1.8,0.1c1.2,0.2 2.2,0.6 3.1,1c1.8,0.9 2.9,2 3.8,2.9c0.4,0.5 0.8,0.9 1,1.3c0.3,0.4 0.5,0.8 0.7,1.1s0.2,0.5 0.3,0.6c0.1,0.1 0.1,0.2 0.1,0.2c0.7,1.8 -0.3,3.8 -2.3,4.5c-1.6,0.7 -3.1,0.4 -4.1,-0.5z"
android:fillColor="#422b0d"/>
<path
android:pathData="M38.87,84.46C37.78,81.38 37,113.28 37,113.28c2.92,1.23 5.87,2.31 8.93,3.13c1.21,-2.73 2.79,-7.76 1.96,-13.46c-1.34,-9.11 -6.78,-12.14 -9.02,-18.49z"
android:strokeAlpha="0.66"
android:fillAlpha="0.66">
<aapt:attr name="android:fillColor">
<gradient
android:startY="128.189"
android:startX="42.556"
android:endY="97.296"
android:endX="42.556"
android:type="linear">
<item android:offset="0" android:color="#FFBF360C"/>
<item android:offset="1" android:color="#33BF360C"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M39.5,81.23c-0.49,-1.33 -1.11,-2.32 -2.68,-2.32c-0.72,0 -1.94,0.18 -2.59,1.17c-1.28,1.92 -0.78,5.52 0.2,8.65c0.55,1.77 0.4,2.42 0.4,2.42c-2.72,-0.17 -5.88,-3.88 -9.03,-7.58c-1.93,-2.27 -3.05,-4.36 -4.93,-6.68c-1.14,-1.41 -2.81,-2.83 -4.77,-2.11c-5.37,1.99 1.27,9.72 2.93,11.84c1.89,2.41 3.76,4.72 3.76,4.72c-1.76,-0.41 -2.87,-0.88 -5.97,-4.06c-2.65,-2.72 -4.62,-6.23 -6.06,-7.54c-3.27,-2.97 -5.27,-2 -6.14,-0.02c-0.81,1.84 0.53,5.09 1.44,6.61c4.51,7.48 10.27,10.41 10.27,10.41c-4.74,-0.21 -8.48,-5.29 -9.99,-6.83c-4.38,-4.45 -6.38,1.06 -3.99,5.05c1.1,1.83 2.2,3.12 4.21,5.19c3.25,3.33 6.31,4.84 6.31,4.84s-1.31,0.2 -2.63,-0.49c-1.78,-0.93 -3.77,-2.33 -4.8,-3.13c-2.95,-2.27 -5.76,-0.32 -4.63,2.7c1.31,3.49 6.09,6.45 8.98,8.6c5.62,4.16 12.63,7.49 19.73,7.26c7.9,-0.25 13.34,-5.68 15.48,-13.31c1.24,-4.44 -1.72,-11.5 -2.47,-13.28c-2.47,-5.76 -2.54,-10.78 -3.03,-12.11z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="38.904"
android:centerX="29.186"
android:centerY="93.866"
android:type="radial">
<item android:offset="0.33" android:color="#FFFFF176"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M23.62,99.08c0.14,8.38 5.4,11.88 9.48,12.65a13.771,13.771 0,0 1,-2.39 -8.2c0.1,-2.9 1.22,-6.51 2.88,-8.47c1.66,-1.96 1.24,-3.89 1.24,-3.89s-1.12,-0.09 -1.54,-0.34c-1.34,-0.82 -2.88,-1.48 -4.04,-2.55c-0.85,-0.79 -5.77,2.42 -5.63,10.8z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="11.727577"
android:centerX="36.794704"
android:centerY="102.449486"
android:type="radial">
<item android:offset="0" android:color="#FFFFC709"/>
<item android:offset="1" android:color="#00FFC709"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M6.4,78.54a2.84,2.84 0,0 0,-1.28 2.35c-0.02,0.91 0.25,1.82 0.58,2.68c0.7,1.73 1.8,3.28 2.93,4.77c1.16,1.48 2.41,2.91 3.71,4.29c1.3,1.39 2.63,2.76 3.98,4.13c-1.67,-0.98 -3.22,-2.16 -4.65,-3.46c-1.42,-1.31 -2.77,-2.73 -3.91,-4.31c-1.17,-1.57 -2.1,-3.32 -2.71,-5.2c-0.28,-0.94 -0.48,-1.95 -0.31,-2.95c0.12,-0.99 0.76,-1.94 1.66,-2.3z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="95.255"
android:startX="18.517"
android:endY="82.775"
android:endX="5.827"
android:type="linear">
<item android:offset="0.127" android:color="#FFB7537A"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M0.55,102.67c0.03,-0.28 0.07,-0.52 0.12,-0.73c-0.07,0.23 -0.11,0.47 -0.12,0.73z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="38.904"
android:centerX="29.186"
android:centerY="93.866"
android:type="radial">
<item android:offset="0.33" android:color="#FFFFF176"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M4.31,106.78c-0.72,-0.69 -1.21,-1.27 -1.58,-1.88c-0.42,-0.7 -0.66,-1.44 -0.68,-2.13c-0.02,-0.77 0.05,-1.34 0.78,-1.9c0.36,-0.28 1.37,-0.18 1.35,-0.19c-0.01,-0.01 -0.03,-0.01 -0.04,-0.02c-0.61,-0.24 -1.18,-0.3 -1.69,-0.22l0.01,-0.01l-0.01,0.01c-0.86,0.14 -1.52,0.69 -1.78,1.5c-0.05,0.21 -0.09,0.45 -0.12,0.73c-0.01,0.43 0.06,0.91 0.25,1.42c0.29,0.77 0.76,1.52 1.33,2.24c0.34,0.52 0.74,1.01 1.11,1.48c0.59,0.7 1.19,1.39 1.86,2.01a26.45,26.45 0,0 0,4.3 3.38c0.75,0.46 8.17,5.01 14.67,6.26c0.02,-0.02 -12.39,-5.14 -19.76,-12.68z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="109.935"
android:startX="0.549"
android:endY="109.935"
android:endX="24.074"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M13.25,105.53c-0.07,0 -0.15,-0.01 -0.21,-0.04c-1.58,-0.66 -3.15,-1.6 -4.81,-2.88a27.08,27.08 0,0 1,-4.01 -3.91c-0.64,-0.74 -1.21,-1.55 -1.67,-2.24l-0.14,-0.22c-0.47,-0.73 -0.95,-1.48 -1.25,-2.36c-0.39,-1.04 -0.49,-2.11 -0.27,-3.1c0.25,-1.19 0.93,-2 1.88,-2.41c1.36,-0.59 2.88,0.91 2.79,0.87c-1.05,-0.47 -1.97,-0.21 -2.21,-0.05c-0.55,0.38 -0.9,1.04 -0.97,1.81c-0.05,0.69 0.11,1.45 0.46,2.19c0.3,0.65 0.72,1.28 1.37,2.04c0.56,0.72 1.14,1.38 1.71,2.03a57.36,57.36 0,0 0,3.68 3.77c1.07,0.99 2.49,2.28 4.02,3.5c0.22,0.17 0.28,0.48 0.13,0.72c-0.12,0.18 -0.3,0.28 -0.5,0.28z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="96.876"
android:startX="0.779"
android:endY="96.876"
android:endX="13.816"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M16.78,96.91c0.19,-0.21 0.15,-0.34 -0.05,-0.54a218.71,218.71 0,0 1,-3.97 -4.12a61.13,61.13 0,0 1,-3.67 -4.24C8,86.58 6.9,85.04 6.22,83.37c-0.26,-0.68 -0.56,-1.58 -0.54,-2.47c0,-0.4 0.11,-0.77 0.29,-1.1c0.17,-0.31 0.42,-0.58 0.73,-0.78c0.01,0 0.01,-0.01 0.01,-0.01c0.51,-0.31 1.34,-0.54 2.67,-0.31c-2.33,-1.57 -4.08,-0.68 -4.82,1.01c-0.81,1.84 0.57,5.13 1.49,6.65c4.51,7.48 10.02,10.47 10.02,10.47c0.27,0.17 0.57,0.22 0.71,0.08z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="87.433"
android:startX="4.074"
android:endY="87.433"
android:endX="16.901"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M23.54,91.61c-1.69,-2.24 -8.47,-9.35 -8.3,-14c0.05,-1.49 2.43,-3.02 4.81,-1.63c0.03,0.02 -1.74,-2 -3.96,-1.21c-1.93,0.68 -2.21,2.46 -2.23,2.78c-0.1,1.01 0.18,1.98 0.45,2.72c0.56,1.4 1.27,2.76 2.29,4.39c0.76,1.24 1.65,2.5 2.79,3.99c0.63,0.81 1.27,1.55 1.92,2.25c0.44,0.48 0.89,0.94 1.35,1.39c0.11,0.1 0.25,0.16 0.39,0.16c0.12,0 0.24,-0.04 0.34,-0.11c0.21,-0.18 0.31,-0.51 0.15,-0.73z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="83.513"
android:startX="13.586"
android:endY="83.513"
android:endX="23.62"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M36.82,78.92c-0.72,0 -1.94,0.18 -2.59,1.17c-1.28,1.92 -0.78,5.52 0.2,8.65c0.16,0.51 0.26,0.92 0.32,1.26a0.794,0.794 0,0 1,-1.06 0.9c-2.45,-0.92 -5.17,-4.13 -7.89,-7.32l-0.41,-0.5c0.03,0.46 1.59,2.96 3.72,5.65c0.57,0.73 4.36,4.86 6.38,3.68c1.25,-0.72 -0.79,-6.32 -0.81,-7.77c-0.01,-0.79 -0.12,-1.81 0.13,-2.75c0.05,-0.24 0.2,-0.49 0.39,-0.74c1.09,-1.46 3.35,-1.52 4.28,0.05l0.03,0.04c-0.5,-1.34 -1.12,-2.32 -2.69,-2.32z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="85.763"
android:startX="25.394"
android:endY="85.763"
android:endX="40.599"
android:type="linear">
<item android:offset="0" android:color="#FFFFC400"/>
<item android:offset="0.873" android:color="#FFDA6727"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M88.31,84.46c1.09,-3.08 1.87,28.82 1.87,28.82c-2.92,1.23 -5.87,2.31 -8.93,3.13c-1.21,-2.73 -2.79,-7.76 -1.96,-13.46c1.34,-9.11 6.77,-12.14 9.02,-18.49z"
android:strokeAlpha="0.66"
android:fillAlpha="0.66">
<aapt:attr name="android:fillColor">
<gradient
android:startY="128.189"
android:startX="84.62198"
android:endY="97.296"
android:endX="84.62198"
android:type="linear">
<item android:offset="0" android:color="#FFBF360C"/>
<item android:offset="1" android:color="#33BF360C"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M87.68,81.23c0.49,-1.33 1.11,-2.32 2.68,-2.32c0.72,0 1.94,0.18 2.59,1.17c1.28,1.92 0.78,5.52 -0.2,8.65c-0.55,1.77 -0.4,2.42 -0.4,2.42c2.72,-0.17 5.88,-3.88 9.03,-7.58c1.93,-2.27 3.05,-4.36 4.93,-6.68c1.14,-1.41 2.81,-2.83 4.77,-2.11c5.37,1.99 -1.27,9.72 -2.93,11.84c-1.89,2.41 -3.76,4.72 -3.76,4.72c1.76,-0.41 2.87,-0.88 5.97,-4.06c2.65,-2.72 4.62,-6.23 6.06,-7.54c3.27,-2.97 5.27,-2 6.14,-0.02c0.81,1.84 -0.53,5.09 -1.44,6.61c-4.51,7.48 -10.27,10.41 -10.27,10.41c4.74,-0.21 8.48,-5.29 9.99,-6.83c4.38,-4.45 6.38,1.06 3.99,5.05c-1.1,1.83 -2.2,3.12 -4.21,5.19c-3.25,3.33 -6.31,4.84 -6.31,4.84s1.31,0.2 2.63,-0.49c1.78,-0.93 3.77,-2.33 4.8,-3.13c2.95,-2.27 5.76,-0.32 4.63,2.7c-1.31,3.49 -6.09,6.45 -8.98,8.6c-5.62,4.16 -12.63,7.49 -19.73,7.26c-7.9,-0.25 -13.34,-5.68 -15.48,-13.31c-1.24,-4.44 1.72,-11.5 2.47,-13.28c2.46,-5.76 2.54,-10.78 3.03,-12.11z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="38.904"
android:centerX="97.99198"
android:centerY="93.866"
android:type="radial">
<item android:offset="0.33" android:color="#FFFFF176"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M103.56,99.08c-0.14,8.38 -5.4,11.88 -9.48,12.65c1.64,-2.39 2.49,-5.31 2.39,-8.2c-0.1,-2.9 -1.22,-6.51 -2.88,-8.47c-1.66,-1.96 -1.24,-3.89 -1.24,-3.89s1.12,-0.09 1.54,-0.34c1.34,-0.82 2.88,-1.48 4.04,-2.55c0.84,-0.79 5.77,2.42 5.63,10.8z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="11.727577"
android:centerX="90.41754"
android:centerY="102.50223"
android:type="radial">
<item android:offset="0" android:color="#FFFFC709"/>
<item android:offset="1" android:color="#00FFC709"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M120.77,78.54a2.84,2.84 0,0 1,1.28 2.35c0.03,0.92 -0.24,1.82 -0.57,2.68c-0.7,1.73 -1.8,3.28 -2.93,4.77a62.626,62.626 0,0 1,-3.71 4.29c-1.3,1.39 -2.63,2.76 -3.98,4.13c1.67,-0.98 3.22,-2.16 4.65,-3.46c1.42,-1.31 2.77,-2.73 3.91,-4.31c1.17,-1.57 2.1,-3.32 2.71,-5.2c0.28,-0.94 0.48,-1.95 0.31,-2.95c-0.13,-0.99 -0.76,-1.94 -1.67,-2.3z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="95.255"
android:startX="108.66098"
android:endY="82.775"
android:endX="121.35098"
android:type="linear">
<item android:offset="0.127" android:color="#FFB7537A"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M126.63,102.67c-0.03,-0.28 -0.07,-0.52 -0.12,-0.73c0.06,0.23 0.11,0.47 0.12,0.73z">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="38.904"
android:centerX="97.99198"
android:centerY="93.866"
android:type="radial">
<item android:offset="0.33" android:color="#FFFFF176"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M122.86,106.78c0.72,-0.69 1.21,-1.27 1.58,-1.88c0.42,-0.7 0.66,-1.44 0.68,-2.13c0.02,-0.77 -0.05,-1.34 -0.78,-1.9c-0.36,-0.28 -1.37,-0.18 -1.35,-0.19c0.01,-0.01 0.03,-0.01 0.04,-0.02c0.61,-0.24 1.18,-0.3 1.69,-0.22l-0.01,-0.01l0.01,0.01c0.86,0.14 1.52,0.69 1.78,1.5c0.05,0.21 0.09,0.45 0.12,0.73c0.01,0.43 -0.06,0.91 -0.25,1.42c-0.29,0.77 -0.76,1.52 -1.33,2.24c-0.34,0.52 -0.74,1.01 -1.11,1.48c-0.59,0.7 -1.19,1.39 -1.86,2.01a26.45,26.45 0,0 1,-4.3 3.38c-0.75,0.46 -8.17,5.01 -14.67,6.26c-0.01,-0.02 12.4,-5.14 19.76,-12.68z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="109.935"
android:startX="126.62798"
android:endY="109.935"
android:endX="103.10398"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M113.92,105.53c0.07,0 0.15,-0.01 0.21,-0.04c1.58,-0.66 3.15,-1.6 4.81,-2.88a27.08,27.08 0,0 0,4.01 -3.91c0.64,-0.74 1.21,-1.55 1.67,-2.24l0.14,-0.22c0.47,-0.73 0.95,-1.48 1.25,-2.36c0.39,-1.04 0.49,-2.11 0.27,-3.1c-0.25,-1.19 -0.93,-2 -1.88,-2.41c-1.36,-0.59 -2.88,0.91 -2.79,0.87c1.05,-0.47 1.97,-0.21 2.21,-0.05c0.55,0.38 0.91,1.04 0.98,1.81c0.05,0.69 -0.11,1.45 -0.46,2.19c-0.3,0.65 -0.72,1.28 -1.37,2.04c-0.56,0.72 -1.14,1.38 -1.71,2.03a57.36,57.36 0,0 1,-3.68 3.77c-1.07,0.99 -2.49,2.28 -4.02,3.5c-0.22,0.17 -0.28,0.48 -0.13,0.72c0.11,0.18 0.3,0.28 0.49,0.28z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="96.876"
android:startX="126.39898"
android:endY="96.876"
android:endX="113.361984"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M110.4,96.91c-0.19,-0.21 -0.15,-0.34 0.05,-0.54c1.49,-1.51 2.79,-2.86 3.97,-4.12a61.13,61.13 0,0 0,3.67 -4.24c1.08,-1.42 2.18,-2.96 2.86,-4.63c0.26,-0.68 0.56,-1.58 0.54,-2.47c0,-0.4 -0.11,-0.77 -0.29,-1.1c-0.17,-0.31 -0.42,-0.58 -0.73,-0.78c-0.01,0 -0.01,-0.01 -0.01,-0.01c-0.51,-0.31 -1.34,-0.54 -2.67,-0.31c2.33,-1.57 4.08,-0.68 4.82,1.01c0.81,1.84 -0.57,5.13 -1.49,6.65c-4.51,7.48 -10.02,10.47 -10.02,10.47c-0.26,0.16 -0.57,0.21 -0.7,0.07z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="87.433"
android:startX="123.10298"
android:endY="87.433"
android:endX="110.275986"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M103.64,91.61c1.69,-2.24 8.47,-9.35 8.3,-14c-0.05,-1.49 -2.43,-3.02 -4.81,-1.63c-0.03,0.02 1.74,-2 3.96,-1.21c1.93,0.68 2.21,2.46 2.23,2.78c0.1,1.01 -0.18,1.98 -0.45,2.72c-0.56,1.4 -1.27,2.76 -2.29,4.39c-0.76,1.24 -1.65,2.5 -2.79,3.99c-0.63,0.81 -1.27,1.55 -1.92,2.25c-0.44,0.48 -0.89,0.94 -1.35,1.39a0.59,0.59 0,0 1,-0.39 0.16c-0.12,0 -0.24,-0.04 -0.34,-0.11c-0.22,-0.18 -0.32,-0.51 -0.15,-0.73z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="83.513"
android:startX="113.59198"
android:endY="83.513"
android:endX="103.55798"
android:type="linear">
<item android:offset="0.127" android:color="#FFDA6727"/>
<item android:offset="1" android:color="#FFFFC400"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M90.36,78.92c0.72,0 1.94,0.18 2.59,1.17c1.28,1.92 0.78,5.52 -0.2,8.65c-0.16,0.51 -0.26,0.92 -0.32,1.26a0.794,0.794 0,0 0,1.06 0.9c2.45,-0.92 5.17,-4.13 7.89,-7.32l0.41,-0.5c-0.03,0.46 -1.59,2.96 -3.72,5.65c-0.57,0.73 -4.36,4.86 -6.38,3.68c-1.25,-0.72 0.79,-6.32 0.81,-7.77c0.01,-0.79 0.12,-1.81 -0.13,-2.75c-0.05,-0.24 -0.2,-0.49 -0.39,-0.74c-1.09,-1.46 -3.35,-1.52 -4.28,0.05l-0.03,0.04c0.5,-1.34 1.11,-2.32 2.69,-2.32z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="85.763"
android:startX="101.78398"
android:endY="85.763"
android:endX="86.57798"
android:type="linear">
<item android:offset="0" android:color="#FFFFC400"/>
<item android:offset="0.873" android:color="#FFDA6727"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M0,0h128v128h-128z"
android:fillColor="#00000000"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -31,7 +31,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:drawablePadding="8dp" android:drawablePadding="8dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:includeFontPadding="false" android:includeFontPadding="false"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -10,7 +11,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:elevation="0dp" android:elevation="0dp"
android:minHeight="30dp" android:minHeight="30dp"
android:theme="@style/AppTheme.AppBarOverlay" android:theme="@style/AppTheme"
tools:targetApi="lollipop" /> tools:targetApi="lollipop" />
<LinearLayout <LinearLayout
@ -29,9 +30,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:drawableStart="@drawable/timer"
android:drawableLeft="@drawable/timer"
android:drawablePadding="8dp" android:drawablePadding="8dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:includeFontPadding="false" android:includeFontPadding="false"
@ -41,6 +39,8 @@
android:textSize="@dimen/text_size" android:textSize="@dimen/text_size"
android:textStyle="bold" android:textStyle="bold"
android:visibility="gone" android:visibility="gone"
app:drawableLeftCompat="@drawable/timer"
app:drawableStartCompat="@drawable/timer"
tools:text="10:00" tools:text="10:00"
tools:visibility="visible" /> tools:visibility="visible" />
@ -49,8 +49,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:drawableStart="@drawable/mine"
android:drawableLeft="@drawable/mine"
android:drawablePadding="8dp" android:drawablePadding="8dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:includeFontPadding="false" android:includeFontPadding="false"
@ -59,6 +57,7 @@
android:textSize="@dimen/text_size" android:textSize="@dimen/text_size"
android:textStyle="bold" android:textStyle="bold"
android:visibility="gone" android:visibility="gone"
app:drawableStartCompat="@drawable/mine"
tools:text="99" tools:text="99"
tools:visibility="visible" /> tools:visibility="visible" />

View file

@ -5,7 +5,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"> android:padding="16dp">
<LinearLayout <LinearLayout

View file

@ -13,13 +13,14 @@
android:layout_height="64dp" android:layout_height="64dp"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="false"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/emoji_smiling_face_with_sunglasses" /> tools:src="@drawable/emoji_smiling_face_with_sunglasses"
tools:ignore="KeyboardInaccessibleWidget" />
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<ImageView
android:id="@+id/emoji"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="24dp"
android:importantForAccessibility="no"
app:srcCompat="@drawable/emoji_hugging_face"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
<TextView
android:id="@+id/supportText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/support"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textSize="18sp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/emoji"/>
<TextView
android:id="@+id/supportDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/support_description"
android:layout_marginTop="12dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:gravity="center"
android:textSize="16sp"
android:paddingBottom="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/supportText"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -9,11 +9,9 @@
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:paddingLeft="32dp"
android:paddingRight="48dp"
android:paddingStart="32dp" android:paddingStart="32dp"
android:paddingEnd="48dp" android:paddingEnd="48dp"
android:paddingVertical="24dp"> android:paddingVertical="12dp">
<androidx.appcompat.widget.AppCompatRadioButton <androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/radio" android:id="@+id/radio"
@ -35,15 +33,6 @@
tools:text=""> tools:text="">
<TableRow> <TableRow>
<TextView
android:id="@+id/title"
android:textStyle="bold"
tools:text="Title" />
</TableRow>
<TableRow android:paddingVertical="4dp">
<TextView <TextView
android:id="@+id/firstAction" android:id="@+id/firstAction"
tools:text="First Action" /> tools:text="First Action" />
@ -62,7 +51,6 @@
</TableRow> </TableRow>
<TableRow> <TableRow>
<TextView <TextView
android:id="@+id/secondAction" android:id="@+id/secondAction"
tools:text="Long Tap" /> tools:text="Long Tap" />

View file

@ -24,7 +24,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/badge" app:layout_constraintStart_toEndOf="@id/badge"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@ -35,7 +34,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/badge" app:layout_constraintStart_toEndOf="@id/badge"
app:layout_constraintTop_toBottomOf="@id/difficulty" app:layout_constraintTop_toBottomOf="@id/difficulty"
@ -46,7 +44,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:text="-" android:text="-"
app:layout_constraintStart_toEndOf="@id/minefieldSize" app:layout_constraintStart_toEndOf="@id/minefieldSize"
@ -58,7 +55,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
app:layout_constraintStart_toEndOf="@id/dash" app:layout_constraintStart_toEndOf="@id/dash"
app:layout_constraintTop_toBottomOf="@id/difficulty" app:layout_constraintTop_toBottomOf="@id/difficulty"

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="?selectableItemBackgroundBorderless"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:drawablePadding="10dp"
app:tint="@android:color/tab_indicator_text"
app:tintMode="src_in"
tools:src="@drawable/games_achievements" />
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:textStyle="bold"
android:layout_marginStart="8dp"
tools:text="Text" />
</LinearLayout>

View file

@ -11,7 +11,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
android:columnCount="3" android:columnCount="3"
android:background="#FFFF0000"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"

View file

@ -71,6 +71,14 @@
android:title="@string/settings" /> android:title="@string/settings" />
</group> </group>
<group android:id="@+id/play_games_group">
<item
android:id="@+id/play_games"
android:checkable="false"
android:icon="@drawable/games_controller"
android:title="@string/google_play_games" />
</group>
<group <group
android:id="@+id/install_group" android:id="@+id/install_group"
android:visible="false" android:visible="false"

View file

@ -3,11 +3,10 @@ package dev.lucasnlm.antimine
import android.os.Build import android.os.Build
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.launchActivity import androidx.test.core.app.launchActivity
import dagger.hilt.android.testing.HiltAndroidRule import dev.lucasnlm.antimine.di.AppModule
import dagger.hilt.android.testing.HiltAndroidTest import dev.lucasnlm.antimine.di.TestCommonModule
import dagger.hilt.android.testing.HiltTestApplication import dev.lucasnlm.antimine.di.TestLevelModule
import dagger.hilt.android.testing.UninstallModules import dev.lucasnlm.antimine.di.ViewModelModule
import dev.lucasnlm.antimine.common.level.di.LevelModule
import dev.lucasnlm.antimine.level.view.EndGameDialogFragment import dev.lucasnlm.antimine.level.view.EndGameDialogFragment
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
@ -15,23 +14,24 @@ import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.test.KoinTestRule
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode import org.robolectric.annotation.LooperMode
import org.robolectric.shadows.ShadowLooper import org.robolectric.shadows.ShadowLooper
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@HiltAndroidTest
@UninstallModules(LevelModule::class)
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P], application = HiltTestApplication::class) @Config(sdk = [Build.VERSION_CODES.P], application = TestApplication::class)
@LooperMode(LooperMode.Mode.PAUSED) @LooperMode(LooperMode.Mode.PAUSED)
class GameActivityTest { class GameActivityTest {
@get:Rule @get:Rule
var rule = HiltAndroidRule(this) val koinTestRule = KoinTestRule.create {
modules(AppModule, TestLevelModule, TestCommonModule, ViewModelModule)
}
@Test @Test
@Ignore("Dagger hilt issue") @Ignore("Disabled until fix touch on tests")
fun testShowGameOverWhenTapAMine() { fun testShowGameOverWhenTapAMine() {
launchActivity<GameActivity>().onActivity { activity -> launchActivity<GameActivity>().onActivity { activity ->
ShadowLooper.runUiThreadTasks() ShadowLooper.runUiThreadTasks()
@ -57,7 +57,7 @@ class GameActivityTest {
} }
@Test @Test
@Ignore("Dagger hilt issue") @Ignore("Disabled until fix touch on tests")
fun testShowVictoryWhenTapAllSafeAreas() { fun testShowVictoryWhenTapAllSafeAreas() {
launchActivity<GameActivity>().onActivity { activity -> launchActivity<GameActivity>().onActivity { activity ->
ShadowLooper.runUiThreadTasks() ShadowLooper.runUiThreadTasks()
@ -70,7 +70,7 @@ class GameActivityTest {
ShadowLooper.runUiThreadTasks() ShadowLooper.runUiThreadTasks()
// Tap on safe places // Tap on safe places
activity.viewModel.field activity.gameViewModel.field
.value!! .value!!
.filter { !it.hasMine && it.isCovered } .filter { !it.hasMine && it.isCovered }
.forEach { .forEach {

View file

@ -0,0 +1,5 @@
package dev.lucasnlm.antimine
import android.app.Application
class TestApplication : Application()

View file

@ -0,0 +1,17 @@
package dev.lucasnlm.antimine.about
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import io.mockk.mockk
import org.junit.Assert.assertTrue
import org.junit.Test
class AboutViewModelTest : IntentViewModelTest() {
@Test
fun testLoad() {
val viewModel = AboutViewModel(mockk())
val state = viewModel.singleState()
assertTrue(state.licenses.isNotEmpty())
assertTrue(state.translators.isNotEmpty())
}
}

View file

@ -1,22 +1,19 @@
package dev.lucasnlm.antimine.control.viewmodel package dev.lucasnlm.antimine.control
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import dev.lucasnlm.antimine.IntentViewModelTest import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.control.viewmodel.ControlEvent
import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel
import dev.lucasnlm.antimine.core.control.ControlStyle import dev.lucasnlm.antimine.core.control.ControlStyle
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test import org.junit.Test
class ControlViewModelTest : IntentViewModelTest() { class ControlViewModelTest : IntentViewModelTest() {
@get:Rule
val rule = InstantTaskExecutorRule()
private fun ControlViewModel.selectedControlStyle() = singleState().let { private fun ControlViewModel.selectedControlStyle() = singleState().let {
it.gameControls[it.selectedId].controlStyle it.gameControls[it.selectedIndex].controlStyle
} }
@Test @Test
@ -36,7 +33,11 @@ class ControlViewModelTest : IntentViewModelTest() {
} }
val viewModel = ControlViewModel(preferenceRepository) val viewModel = ControlViewModel(preferenceRepository)
viewModel.sendEvent(ControlEvent.SelectControlStyle(ControlStyle.FastFlag)) viewModel.sendEvent(
ControlEvent.SelectControlStyle(
ControlStyle.FastFlag
)
)
assertEquals(ControlStyle.FastFlag, viewModel.selectedControlStyle()) assertEquals(ControlStyle.FastFlag, viewModel.selectedControlStyle())
verify { preferenceRepository.useControlStyle(ControlStyle.FastFlag) } verify { preferenceRepository.useControlStyle(ControlStyle.FastFlag) }
} }

View file

@ -0,0 +1,43 @@
package dev.lucasnlm.antimine.custom
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.custom.viewmodel.CreateGameViewModel
import dev.lucasnlm.antimine.custom.viewmodel.CustomEvent
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Test
class CreateGameViewModelTest : IntentViewModelTest() {
@Test
fun testInitialValue() {
val preferenceRepository: IPreferencesRepository = mockk {
every { customGameMode() } returns Minefield(10, 12, 9)
}
val result = CreateGameViewModel(preferenceRepository).singleState()
assertEquals(10, result.width)
assertEquals(12, result.height)
assertEquals(9, result.mines)
}
@Test
fun testSetNewCustomValues() {
val preferenceRepository: IPreferencesRepository = mockk {
every { customGameMode() } returns Minefield(10, 12, 9)
every { updateCustomGameMode(any()) } returns Unit
}
val result = CreateGameViewModel(preferenceRepository).run {
sendEvent(CustomEvent.UpdateCustomGameEvent(Minefield(9, 8, 5)))
singleState()
}
assertEquals(9, result.width)
assertEquals(8, result.height)
assertEquals(5, result.mines)
}
}

View file

@ -0,0 +1,58 @@
package dev.lucasnlm.antimine.di
import android.app.Activity
import android.content.Context
import android.content.Intent
import dev.lucasnlm.antimine.core.analytics.DebugAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.share.ShareManager
import dev.lucasnlm.external.IBillingManager
import dev.lucasnlm.external.IInstantAppManager
import dev.lucasnlm.external.IPlayGamesManager
import org.koin.dsl.bind
import org.koin.dsl.module
val AppModule = module {
single {
object : IInstantAppManager {
override fun isEnabled(context: Context): Boolean = false
override fun showInstallPrompt(
activity: Activity,
intent: Intent?,
requestCode: Int,
referrer: String?
): Boolean = false
}
} bind IInstantAppManager::class
single {
object : IBillingManager {
override fun start() { }
override suspend fun charge(activity: Activity) { }
}
} bind IBillingManager::class
single { object : IPlayGamesManager {
override fun hasGooglePlayGames(): Boolean = false
override fun silentLogin(activity: Activity) { }
override fun getLoginIntent(): Intent? = null
override fun handleLoginResult(data: Intent?) { }
override fun isLogged(): Boolean = false
override fun openAchievements(activity: Activity) { }
override fun openLeaderboards(activity: Activity) { }
} } bind IPlayGamesManager::class
single { ShareManager(get()) }
single {
DebugAnalyticsManager()
} bind IAnalyticsManager::class
}

View file

@ -0,0 +1,32 @@
package dev.lucasnlm.antimine.di
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.sound.ISoundManager
import dev.lucasnlm.antimine.core.themes.model.AppTheme
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.core.themes.repository.Themes.LightTheme
import dev.lucasnlm.antimine.mocks.FixedDimensionRepository
import dev.lucasnlm.antimine.mocks.MockPreferencesRepository
import org.koin.dsl.bind
import org.koin.dsl.module
val TestCommonModule = module {
single { FixedDimensionRepository() } bind IDimensionRepository::class
single { MockPreferencesRepository() } bind IPreferencesRepository::class
single { object : ISoundManager {
override fun play(soundId: Int) { }
} } bind ISoundManager::class
single { object : IThemeRepository {
override fun getCustomTheme(): AppTheme? = null
override fun getTheme(): AppTheme = LightTheme
override fun getAllThemes(): List<AppTheme> = listOf(LightTheme)
override fun setTheme(theme: AppTheme) { }
} } bind IThemeRepository::class
}

View file

@ -1,11 +1,5 @@
package dev.lucasnlm.antimine.di package dev.lucasnlm.antimine.di
import androidx.lifecycle.MutableLiveData
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dev.lucasnlm.antimine.common.level.models.Event
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
@ -15,25 +9,27 @@ import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackManager import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackManager
import dev.lucasnlm.antimine.mocks.DisabledHapticFeedbackManager import dev.lucasnlm.antimine.mocks.DisabledHapticFeedbackManager
import dev.lucasnlm.antimine.mocks.FixedMinefieldRepository import dev.lucasnlm.antimine.mocks.FixedMinefieldRepository
import org.koin.dsl.bind
import org.koin.dsl.module
@Module val TestLevelModule = module {
@InstallIn(ActivityComponent::class) single {
class TestLevelModule { Clock()
@Provides }
fun provideGameEventObserver(): MutableLiveData<Event> = MutableLiveData()
@Provides single {
fun provideClock(): Clock = Clock() MemorySavesRepository()
} bind ISavesRepository::class
@Provides single {
fun provideSavesRepository(): ISavesRepository = MemorySavesRepository() MemoryStatsRepository()
} bind IStatsRepository::class
@Provides single {
fun provideStatsRepository(): IStatsRepository = MemoryStatsRepository() FixedMinefieldRepository()
} bind IMinefieldRepository::class
@Provides single {
fun provideMinefieldRepository(): IMinefieldRepository = FixedMinefieldRepository() DisabledHapticFeedbackManager()
} bind IHapticFeedbackManager::class
@Provides
fun provideHapticFeedbackInteractor(): IHapticFeedbackManager = DisabledHapticFeedbackManager()
} }

View file

@ -0,0 +1,53 @@
package dev.lucasnlm.antimine.history
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.common.level.database.models.FirstOpen
import dev.lucasnlm.antimine.common.level.database.models.Save
import dev.lucasnlm.antimine.common.level.database.models.SaveStatus
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.history.viewmodel.HistoryEvent
import dev.lucasnlm.antimine.history.viewmodel.HistoryState
import dev.lucasnlm.antimine.history.viewmodel.HistoryViewModel
import io.mockk.coEvery
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Test
class HistoryViewModelTest : IntentViewModelTest() {
private val fakeMinefield = Minefield(9, 9, 9)
private val allSaves = listOf(
Save(
0, 1, 0L, 100L, fakeMinefield,
Difficulty.Beginner, FirstOpen.Unknown, SaveStatus.ON_GOING, listOf()
),
Save(
1, 2, 0L, 100L, fakeMinefield,
Difficulty.Beginner, FirstOpen.Unknown, SaveStatus.ON_GOING, listOf()
),
Save(
2, 3, 0L, 100L, fakeMinefield,
Difficulty.Beginner, FirstOpen.Unknown, SaveStatus.ON_GOING, listOf()
)
)
@Test
fun testInitialValue() {
val viewModel = HistoryViewModel(mockk(), mockk())
assertEquals(HistoryState(listOf()), viewModel.singleState())
}
@Test
fun testLoadHistory() {
val savesRepository = mockk<ISavesRepository> {
coEvery { getAllSaves() } returns allSaves
}
val state = HistoryViewModel(mockk(), savesRepository).run {
sendEvent(HistoryEvent.LoadAllSaves)
singleState()
}
assertEquals(HistoryState(allSaves.sortedByDescending { it.uid }), state)
}
}

View file

@ -0,0 +1,59 @@
package dev.lucasnlm.antimine.mocks
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.core.control.ControlStyle
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
class MockPreferencesRepository : IPreferencesRepository {
var customMinefield = Minefield(9, 9, 9)
override fun customGameMode(): Minefield = customMinefield
override fun updateCustomGameMode(minefield: Minefield) {
customMinefield = minefield
}
override fun controlStyle(): ControlStyle = ControlStyle.Standard
override fun useControlStyle(controlStyle: ControlStyle) { }
override fun isFirstUse(): Boolean = false
override fun completeFirstUse() { }
override fun customLongPressTimeout(): Long = 400L
override fun themeId(): Long = 1L
override fun useTheme(themeId: Long) { }
override fun updateStatsBase(statsBase: Int) { }
override fun getStatsBase(): Int = 0
override fun getUseCount(): Int = 10
override fun incrementUseCount() { }
override fun incrementProgressiveValue() { }
override fun decrementProgressiveValue() { }
override fun getProgressiveValue(): Int = 0
override fun isRequestRatingEnabled(): Boolean = false
override fun disableRequestRating() { }
override fun useFlagAssistant(): Boolean = false
override fun useHapticFeedback(): Boolean = true
override fun areaSizeMultiplier(): Int = 50
override fun useAnimations(): Boolean = false
override fun useQuestionMark(): Boolean = false
override fun isSoundEffectsEnabled(): Boolean = false
}

View file

@ -0,0 +1,149 @@
package dev.lucasnlm.antimine.theme
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.common.R
import dev.lucasnlm.antimine.core.themes.model.AppTheme
import dev.lucasnlm.antimine.core.themes.model.AreaPalette
import dev.lucasnlm.antimine.core.themes.model.Assets
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.theme.viewmodel.ThemeEvent
import dev.lucasnlm.antimine.theme.viewmodel.ThemeState
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Test
class ThemeViewModelTest : IntentViewModelTest() {
private val lightTheme = AppTheme(
id = 1L,
theme = R.style.CustomLightTheme,
themeNoActionBar = R.style.CustomLightTheme_NoActionBar,
palette = AreaPalette(
border = 0x424242,
background = 0xFFFFFF,
covered = 0x424242,
coveredOdd = 0x424242,
uncovered = 0xd5d2cc,
uncoveredOdd = 0xd5d2cc,
minesAround1 = 0x527F8D,
minesAround2 = 0x2B8D43,
minesAround3 = 0xE65100,
minesAround4 = 0x20A5f7,
minesAround5 = 0xED1C24,
minesAround6 = 0xFFC107,
minesAround7 = 0x66126B,
minesAround8 = 0x000000,
highlight = 0x212121,
focus = 0xD32F2F
),
assets = Assets(
wrongFlag = R.drawable.red_flag,
flag = R.drawable.flag,
questionMark = R.drawable.question,
toolbarMine = R.drawable.mine,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low
)
)
private val darkTheme = AppTheme(
id = 2L,
theme = R.style.CustomDarkTheme,
themeNoActionBar = R.style.CustomDarkTheme_NoActionBar,
palette = AreaPalette(
border = 0x171717,
background = 0x212121,
covered = 0x171717,
coveredOdd = 0x171717,
uncovered = 0x424242,
uncoveredOdd = 0x424242,
minesAround1 = 0xd5d2cc,
minesAround2 = 0xd5d2cc,
minesAround3 = 0xd5d2cc,
minesAround4 = 0xd5d2cc,
minesAround5 = 0xd5d2cc,
minesAround6 = 0xd5d2cc,
minesAround7 = 0xd5d2cc,
minesAround8 = 0xd5d2cc,
highlight = 0xFFFFFF,
focus = 0xFFFFFF
),
assets = Assets(
wrongFlag = R.drawable.flag,
flag = R.drawable.flag,
questionMark = R.drawable.question,
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_white,
mineLow = R.drawable.mine_low
)
)
private val gardenTheme = AppTheme(
id = 3L,
theme = R.style.CustomGardenTheme,
themeNoActionBar = R.style.CustomGardenTheme_NoActionBar,
palette = AreaPalette(
border = 0x171717,
background = 0xefebe9,
covered = 0x689f38,
coveredOdd = 0x558b2f,
uncovered = 0xefebe9,
uncoveredOdd = 0xd7ccc8,
minesAround1 = 0x527F8D,
minesAround2 = 0x2B8D43,
minesAround3 = 0xE65100,
minesAround4 = 0x20A5f7,
minesAround5 = 0xED1C24,
minesAround6 = 0xFFC107,
minesAround7 = 0x66126B,
minesAround8 = 0x000000,
highlight = 0xFFFFFF,
focus = 0xFFFFFF
),
assets = Assets(
wrongFlag = R.drawable.flag,
flag = R.drawable.flag,
questionMark = R.drawable.question,
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_white,
mineLow = R.drawable.mine_low
)
)
private val allThemes = listOf(
lightTheme, darkTheme, gardenTheme
)
@Test
fun testInitialValue() {
val themeRepository = mockk<IThemeRepository> {
every { getAllThemes() } returns allThemes
every { getTheme() } returns gardenTheme
}
val viewModel = ThemeViewModel(themeRepository)
assertEquals(ThemeState(gardenTheme, allThemes), viewModel.singleState())
}
@Test
fun testChangeValue() {
val themeRepository = mockk<IThemeRepository> {
every { getAllThemes() } returns allThemes
every { getTheme() } returns gardenTheme
every { setTheme(any()) } returns Unit
}
val state = ThemeViewModel(themeRepository).run {
sendEvent(ThemeEvent.ChangeTheme(darkTheme))
singleState()
}
assertEquals(ThemeState(darkTheme, allThemes), state)
verify { themeRepository.setTheme(darkTheme) }
}
}

View file

@ -1,2 +0,0 @@
sdk=28
application=dagger.hilt.android.testing.HiltTestApplication

View file

@ -6,9 +6,12 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.0' classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28.1-alpha'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72'
if (System.getenv('IS_GOOGLE_BUILD')) {
classpath 'com.google.gms:google-services:4.3.3'
}
} }
} }

View file

@ -2,16 +2,15 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android { android {
compileSdkVersion 30 compileSdkVersion 30
defaultConfig { defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid // versionCode and versionName must be hardcoded to support F-droid
versionCode 800041 versionCode 800051
versionName '8.0.4' versionName '8.0.5'
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
@ -33,14 +32,11 @@ kapt {
correctErrorTypes true correctErrorTypes true
} }
hilt {
enableTransformForLocalTests true
}
dependencies { dependencies {
// Dependencies must be hardcoded to support F-droid // Dependencies must be hardcoded to support F-droid
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':external')
// AndroidX // AndroidX
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
@ -56,13 +52,10 @@ dependencies {
api 'android.arch.lifecycle:extensions:1.1.1' api 'android.arch.lifecycle:extensions:1.1.1'
implementation 'android.arch.lifecycle:viewmodel:1.1.1' implementation 'android.arch.lifecycle:viewmodel:1.1.1'
// Dagger // Koin
implementation 'com.google.dagger:hilt-android:2.28.1-alpha' implementation 'org.koin:koin-android:2.1.6'
kapt 'com.google.dagger:hilt-android-compiler:2.28.1-alpha' implementation 'org.koin:koin-androidx-viewmodel:2.1.6'
testImplementation 'com.google.dagger:hilt-android-testing:2.28.1-alpha' testImplementation 'org.koin:koin-test:2.1.6'
kaptTest 'com.google.dagger:hilt-android-compiler:2.28.1-alpha'
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01"
kapt "androidx.hilt:hilt-compiler:1.0.0-alpha01"
// Room // Room
api 'androidx.room:room-runtime:2.2.5' api 'androidx.room:room-runtime:2.2.5'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 864 B

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 969 B

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 865 B

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,001 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Some files were not shown because too many files have changed in this diff Show more