Merge pull request #175 from lucasnlm/small-improvements

Small improvements
This commit is contained in:
Lucas Nunes 2020-09-30 11:48:03 -03:00 committed by GitHub
commit 5a60c4ba90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 904 additions and 365 deletions

View file

@ -12,8 +12,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 801011
versionName '8.1.1'
versionCode 802001
versionName '8.2.0'
minSdkVersion 21
targetSdkVersion 30
multiDexEnabled true

View file

@ -5,6 +5,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.format.DateUtils
import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
@ -30,6 +31,7 @@ import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.control.ControlDialogFragment
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.models.Analytics
import dev.lucasnlm.antimine.core.control.ControlStyle
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.custom.CustomLevelDialogFragment
import dev.lucasnlm.antimine.history.HistoryActivity
@ -92,6 +94,8 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
private val currentRadius by lazy { preferencesRepository.squareRadius() }
private val useHelp by lazy { preferencesRepository.useHelp() }
private var gameToast: Toast? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
@ -103,6 +107,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
bindToolbar()
bindDrawer()
bindNavigationMenu()
bindSwitchControlButton()
bindAds()
findViewById<FrameLayout>(R.id.levelContainer).doOnLayout {
@ -267,6 +272,30 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
}
private fun bindSwitchControlButton() {
switchFlag.apply {
visibility = if (preferencesRepository.controlStyle() == ControlStyle.SwitchMarkOpen) {
View.VISIBLE
} else {
View.GONE
}
TooltipCompat.setTooltipText(this, getString(R.string.switch_control))
setImageResource(R.drawable.touch)
setColorFilter(minesCount.currentTextColor)
setOnClickListener {
if (preferencesRepository.openUsingSwitchControl()) {
gameViewModel.refreshUseOpenOnSwitchControl(false)
preferencesRepository.setSwitchControl(false)
setImageResource(R.drawable.flag_black)
} else {
gameViewModel.refreshUseOpenOnSwitchControl(true)
preferencesRepository.setSwitchControl(true)
setImageResource(R.drawable.touch)
}
}
}
}
private fun refreshInGameShortcut() {
if (preferencesRepository.useHelp()) {
refreshTipShortcutIcon()
@ -606,14 +635,37 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
}
private fun waitAndShowEndGameDialog(victory: Boolean, await: Boolean) {
private fun showEndGameToast(victory: Boolean) {
gameToast?.cancel()
val message = if (victory) { R.string.you_won } else { R.string.you_won }
gameToast = Toast.makeText(this, message, Toast.LENGTH_LONG).apply {
setGravity(Gravity.CENTER, 0, 0)
show()
}
}
private fun showEndGameAlert(victory: Boolean) {
val canShowWindow = preferencesRepository.showWindowsWhenFinishGame()
if (!isFinishing) {
if (canShowWindow) {
showEndGameDialog(victory)
} else {
showEndGameToast(victory)
}
}
}
private fun waitAndShowEndGameAlert(victory: Boolean, await: Boolean) {
if (await && gameViewModel.explosionDelay() != 0L) {
lifecycleScope.launch {
delay((gameViewModel.explosionDelay() * 0.3).toLong())
showEndGameDialog(victory)
showEndGameAlert(victory)
}
} else {
showEndGameDialog(victory)
showEndGameAlert(victory)
}
}
@ -643,6 +695,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
refreshAds()
}
Event.StartNewGame -> {
gameToast?.cancel()
status = Status.PreGame
disableShortcutIcon()
refreshAds()
@ -686,7 +739,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (!isResuming) {
gameViewModel.addNewTip()
waitAndShowEndGameDialog(
waitAndShowEndGameAlert(
victory = true,
await = false
)
@ -707,7 +760,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (!isResuming) {
GlobalScope.launch(context = Dispatchers.Main) {
gameViewModel.gameOver(isResuming)
waitAndShowEndGameDialog(
waitAndShowEndGameAlert(
victory = false,
await = true
)
@ -774,6 +827,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
resumeGame()
}
bindSwitchControlButton()
refreshAds()
}

View file

@ -10,8 +10,10 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.control.view.ControlItemView
import dev.lucasnlm.antimine.control.view.SimpleControlItemView
import dev.lucasnlm.antimine.control.viewmodel.ControlEvent
import dev.lucasnlm.antimine.control.viewmodel.ControlViewModel
import dev.lucasnlm.antimine.core.control.ControlStyle
import org.koin.androidx.viewmodel.ext.android.viewModel
class ControlDialogFragment : AppCompatDialogFragment() {
@ -40,21 +42,41 @@ class ControlDialogFragment : AppCompatDialogFragment() {
private val controlList = controlViewModel.singleState().gameControls
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view = if (convertView == null) {
ControlItemView(parent!!.context)
if (getItemViewType(position) == USE_COMMON_CONTROL_TYPE) {
val view = if (convertView == null) {
ControlItemView(parent!!.context)
} else {
(convertView as ControlItemView)
}
val selected = controlViewModel.singleState().selected
return view.apply {
val controlModel = controlList[position]
bind(controlModel)
setRadio(selected == controlModel.controlStyle)
setOnClickListener {
controlViewModel.sendEvent(ControlEvent.SelectControlStyle(controlModel.controlStyle))
notifyDataSetChanged()
}
}
} else {
(convertView as ControlItemView)
}
val view = if (convertView == null) {
SimpleControlItemView(parent!!.context)
} else {
(convertView as SimpleControlItemView)
}
val selected = controlViewModel.singleState().selected
val selected = controlViewModel.singleState().selected
return view.apply {
val controlModel = controlList[position]
bind(controlModel)
setRadio(selected == controlModel.controlStyle)
setOnClickListener {
controlViewModel.sendEvent(ControlEvent.SelectControlStyle(controlModel.controlStyle))
notifyDataSetChanged()
return view.apply {
val controlModel = controlList[position]
bind(controlModel)
setRadio(selected == controlModel.controlStyle)
setOnClickListener {
controlViewModel.sendEvent(ControlEvent.SelectControlStyle(controlModel.controlStyle))
notifyDataSetChanged()
}
}
}
}
@ -66,9 +88,21 @@ class ControlDialogFragment : AppCompatDialogFragment() {
override fun getItemId(position: Int): Long = controlList[position].id
override fun getCount(): Int = controlList.count()
override fun getItemViewType(position: Int): Int {
return if (controlList[position].controlStyle == ControlStyle.SwitchMarkOpen) {
USE_SIMPLE_CONTROL_TYPE
} else {
USE_COMMON_CONTROL_TYPE
}
}
override fun getViewTypeCount(): Int = 2
}
companion object {
val TAG = ControlDialogFragment::class.simpleName!!
private const val USE_COMMON_CONTROL_TYPE = 1
private const val USE_SIMPLE_CONTROL_TYPE = 0
}
}

View file

@ -0,0 +1,45 @@
package dev.lucasnlm.antimine.control.view
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatRadioButton
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.control.models.ControlDetails
class SimpleControlItemView : 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)
private val radio: AppCompatRadioButton
private val root: View
private val firstAction: TextView
init {
LayoutInflater
.from(context)
.inflate(R.layout.view_control_item_simple, this, true)
radio = findViewById(R.id.radio)
root = findViewById(R.id.root)
firstAction = findViewById(R.id.firstAction)
}
fun bind(controlDetails: ControlDetails) {
firstAction.text = context.getString(controlDetails.firstActionId)
}
fun setRadio(selected: Boolean) {
radio.isChecked = selected
}
override fun setOnClickListener(listener: OnClickListener?) {
super.setOnClickListener(listener)
root.setOnClickListener(listener)
radio.setOnClickListener(listener)
}
}

View file

@ -18,7 +18,7 @@ class ControlViewModel(
firstActionId = R.string.single_click,
firstActionResponseId = R.string.open_tile,
secondActionId = R.string.long_press,
secondActionResponseId = R.string.flag_tile
secondActionResponseId = R.string.flag_tile,
),
ControlDetails(
id = 1L,
@ -26,7 +26,7 @@ class ControlViewModel(
firstActionId = R.string.single_click,
firstActionResponseId = R.string.flag_tile,
secondActionId = R.string.long_press,
secondActionResponseId = R.string.open_tile
secondActionResponseId = R.string.open_tile,
),
ControlDetails(
id = 2L,
@ -34,7 +34,7 @@ class ControlViewModel(
firstActionId = R.string.single_click,
firstActionResponseId = R.string.flag_tile,
secondActionId = R.string.double_click,
secondActionResponseId = R.string.open_tile
secondActionResponseId = R.string.open_tile,
),
ControlDetails(
id = 3L,
@ -42,7 +42,15 @@ class ControlViewModel(
firstActionId = R.string.single_click,
firstActionResponseId = R.string.open_tile,
secondActionId = R.string.double_click,
secondActionResponseId = R.string.flag_tile
secondActionResponseId = R.string.flag_tile,
),
ControlDetails(
id = 4L,
controlStyle = ControlStyle.SwitchMarkOpen,
firstActionId = R.string.switch_control_desc,
firstActionResponseId = 0,
secondActionId = 0,
secondActionResponseId = 0,
)
)

View file

@ -21,7 +21,7 @@ val ViewModelModule = module {
viewModel { HistoryViewModel(get(), get(), get()) }
viewModel { EndGameDialogViewModel(get()) }
viewModel { PlayGamesViewModel(get(), get()) }
viewModel { StatsViewModel(get(), get()) }
viewModel { StatsViewModel(get(), get(), get(), get()) }
viewModel { TextViewModel(get()) }
viewModel { ThemeViewModel(get(), get(), get(), get()) }
viewModel {

View file

@ -79,6 +79,10 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
}
}
findViewById<View>(R.id.close).setOnClickListener {
dismissAllowingStateLoss()
}
if (state.isVictory == true) {
if (!instantAppManager.isEnabled(context)) {
setNeutralButton(R.string.share) { _, _ ->

View file

@ -6,14 +6,15 @@ import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.ThematicActivity
import dev.lucasnlm.antimine.stats.model.StatsModel
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.stats.view.StatsAdapter
import dev.lucasnlm.antimine.stats.viewmodel.StatsEvent
import dev.lucasnlm.antimine.stats.viewmodel.StatsViewModel
import dev.lucasnlm.external.IInstantAppManager
import kotlinx.android.synthetic.main.activity_stats.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
@ -21,17 +22,23 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
class StatsActivity : ThematicActivity(R.layout.activity_stats) {
private val statsViewModel by viewModel<StatsViewModel>()
private val instantAppManager: IInstantAppManager by inject()
private val themeRepository: IThemeRepository by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
recyclerView.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
}
lifecycleScope.launchWhenResumed {
statsViewModel.sendEvent(StatsEvent.LoadStats)
statsViewModel.observeState().collect {
refreshStats(it)
recyclerView.adapter = StatsAdapter(it.stats, themeRepository)
empty.visibility = if (it.stats.isEmpty()) View.VISIBLE else View.GONE
if (it.showAds && !instantAppManager.isEnabled(applicationContext)) {
ad_placeholder.visibility = View.VISIBLE
@ -41,35 +48,9 @@ class StatsActivity : ThematicActivity(R.layout.activity_stats) {
}
}
private fun refreshStats(stats: StatsModel) {
invalidateOptionsMenu()
if (stats.totalGames > 0) {
minesCount.text = stats.mines.toString()
totalTime.text = formatTime(stats.duration)
averageTime.text = formatTime(stats.averageDuration)
totalGames.text = stats.totalGames.toString()
performance.text = formatPercentage(100.0 * stats.victory / stats.totalGames)
openAreas.text = stats.openArea.toString()
victory.text = stats.victory.toString()
defeat.text = (stats.totalGames - stats.victory).toString()
} else {
val emptyText = "-"
totalGames.text = "0"
minesCount.text = emptyText
totalTime.text = emptyText
averageTime.text = emptyText
performance.text = emptyText
openAreas.text = emptyText
victory.text = emptyText
defeat.text = emptyText
}
invalidateOptionsMenu()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
statsViewModel.singleState().let {
if (it.totalGames > 0) {
if (it.stats.isNotEmpty()) {
menuInflater.inflate(R.menu.delete_icon_menu, menu)
}
}
@ -91,18 +72,10 @@ class StatsActivity : ThematicActivity(R.layout.activity_stats) {
.setMessage(R.string.delete_all_message)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete_all) { _, _ ->
GlobalScope.launch {
lifecycleScope.launch {
statsViewModel.sendEvent(StatsEvent.DeleteStats)
}
}
.show()
}
companion object {
private fun formatPercentage(value: Double) =
String.format("%.2f%%", value)
private fun formatTime(durationSecs: Long) =
String.format("%02d:%02d:%02d", durationSecs / 3600, durationSecs % 3600 / 60, durationSecs % 60)
}
}

View file

@ -1,11 +1,14 @@
package dev.lucasnlm.antimine.stats.model
import androidx.annotation.StringRes
data class StatsModel(
@StringRes val title: Int,
val totalGames: Int,
val duration: Long,
val averageDuration: Long,
val totalTime: Long,
val averageTime: Long,
val shortestTime: Long,
val mines: Int,
val victory: Int,
val openArea: Int,
val showAds: Boolean,
)

View file

@ -0,0 +1,6 @@
package dev.lucasnlm.antimine.stats.model
data class StatsState(
val stats: List<StatsModel>,
val showAds: Boolean
)

View file

@ -0,0 +1,96 @@
package dev.lucasnlm.antimine.stats.view
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.core.themes.repository.IThemeRepository
import dev.lucasnlm.antimine.stats.model.StatsModel
import kotlinx.android.synthetic.main.view_stats.view.*
class StatsAdapter(
private val statsList: List<StatsModel>,
private val themeRepository: IThemeRepository,
) : RecyclerView.Adapter<StatsViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatsViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(R.layout.view_stats, parent, false)
return StatsViewHolder(view)
}
override fun onBindViewHolder(holder: StatsViewHolder, position: Int) {
val stats = statsList[position]
holder.apply {
val color = with(themeRepository.getTheme().palette.background) {
Color.rgb(Color.red(this), Color.green(this), Color.blue(this))
}
card.setCardBackgroundColor(color)
val textColor = with(themeRepository.getTheme().palette.covered) {
Color.rgb(Color.red(this), Color.green(this), Color.blue(this))
}
statsLabel.setTextColor(textColor)
if (stats.totalGames > 0) {
val emptyText = "-"
if (stats.title != 0) {
statsLabel.text = holder.itemView.context.getString(stats.title)
statsLabel.visibility = View.VISIBLE
} else {
statsLabel.visibility = View.GONE
}
minesCount.text = stats.mines.toString()
totalTime.text = formatTime(stats.totalTime)
averageTime.text = formatTime(stats.averageTime)
shortestTime.text = if (stats.shortestTime == 0L) emptyText else formatTime(stats.shortestTime)
totalGames.text = stats.totalGames.toString()
performance.text = formatPercentage(100.0 * stats.victory / stats.totalGames)
openAreas.text = stats.openArea.toString()
victory.text = stats.victory.toString()
defeat.text = (stats.totalGames - stats.victory).toString()
} else {
val emptyText = "-"
statsLabel.visibility = View.GONE
totalGames.text = "0"
minesCount.text = emptyText
totalTime.text = emptyText
averageTime.text = emptyText
shortestTime.text = emptyText
performance.text = emptyText
openAreas.text = emptyText
victory.text = emptyText
defeat.text = emptyText
}
}
}
override fun getItemCount(): Int = statsList.size
companion object {
private fun formatPercentage(value: Double) =
String.format("%.2f%%", value)
private fun formatTime(durationSecs: Long) =
String.format("%02d:%02d:%02d", durationSecs / 3600, durationSecs % 3600 / 60, durationSecs % 60)
}
}
class StatsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val card: CardView = itemView.card
val statsLabel: TextView = itemView.statsLabel
val totalGames: TextView = itemView.totalGames
val minesCount: TextView = itemView.minesCount
val totalTime: TextView = itemView.totalTime
val averageTime: TextView = itemView.averageTime
val shortestTime: TextView = itemView.shortestTime
val openAreas: TextView = itemView.openAreas
val performance: TextView = itemView.performance
val victory: TextView = itemView.victory
val defeat: TextView = itemView.defeat
}

View file

@ -1,53 +1,66 @@
package dev.lucasnlm.antimine.stats.viewmodel
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.database.models.Stats
import dev.lucasnlm.antimine.common.level.models.Difficulty
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.core.viewmodel.IntentViewModel
import dev.lucasnlm.antimine.stats.model.StatsModel
import dev.lucasnlm.antimine.stats.model.StatsState
import kotlinx.coroutines.flow.flow
class StatsViewModel(
private val statsRepository: IStatsRepository,
private val preferenceRepository: IPreferencesRepository,
) : IntentViewModel<StatsEvent, StatsModel>() {
private suspend fun loadStatsModel(): StatsModel {
private val minefieldRepository: IMinefieldRepository,
private val dimensionRepository: IDimensionRepository,
) : IntentViewModel<StatsEvent, StatsState>() {
private suspend fun loadStatsModel(): List<StatsModel> {
val minId = preferenceRepository.getStatsBase()
val stats = statsRepository.getAllStats(minId)
val statsCount = stats.count()
val standardSize = minefieldRepository.fromDifficulty(
Difficulty.Standard,
dimensionRepository,
preferenceRepository,
)
return if (statsCount > 0) {
val result = stats.fold(
StatsModel(
totalGames = statsCount,
duration = 0,
averageDuration = 0,
mines = 0,
victory = 0,
openArea = 0,
showAds = !preferenceRepository.isPremiumEnabled(),
)
) { acc, value ->
StatsModel(
acc.totalGames,
acc.duration + value.duration,
0,
acc.mines + value.mines,
acc.victory + value.victory,
acc.openArea + value.openArea,
showAds = !preferenceRepository.isPremiumEnabled(),
)
}
result.copy(averageDuration = result.duration / result.totalGames)
} else {
StatsModel(
totalGames = 0,
duration = 0,
averageDuration = 0,
mines = 0,
victory = 0,
openArea = 0,
showAds = !preferenceRepository.isPremiumEnabled()
)
return listOf(
// General
stats.fold().copy(title = R.string.general),
// Standard
stats.filter {
it.width == standardSize.width && it.height == standardSize.height
}.fold().copy(title = R.string.standard),
// Expert
stats.filter {
it.mines == 99 && it.width == 24 && it.height == 24
}.fold().copy(title = R.string.expert),
// Intermediate
stats.filter {
it.mines == 40 && it.width == 16 && it.height == 16
}.fold().copy(title = R.string.intermediate),
// Beginner
stats.filter {
it.mines == 10 && it.width == 9 && it.height == 9
}.fold().copy(title = R.string.beginner),
// Custom
stats.filterNot {
it.mines == 99 && it.width == 24 && it.height == 24
}.filterNot {
it.mines == 40 && it.width == 16 && it.height == 16
}.filterNot {
it.mines == 10 && it.width == 9 && it.height == 9
}.fold().copy(title = R.string.custom),
).filter {
it.totalGames > 0
}
}
@ -57,33 +70,67 @@ class StatsViewModel(
}
}
override fun initialState() = StatsModel(
totalGames = 0,
duration = 0,
averageDuration = 0,
mines = 0,
victory = 0,
openArea = 0,
private fun List<Stats>.fold(): StatsModel {
return if (size > 0) {
val result = fold(
StatsModel(
title = 0,
totalGames = size,
totalTime = 0,
averageTime = 0,
shortestTime = 0,
mines = 0,
victory = 0,
openArea = 0,
)
) { acc, value ->
StatsModel(
0,
acc.totalGames,
acc.totalTime + value.duration,
0,
shortestTime = if (value.victory != 0) {
if (acc.shortestTime == 0L) {
value.duration
} else {
acc.shortestTime.coerceAtMost(value.duration)
}
} else {
acc.shortestTime
},
acc.mines + value.mines,
acc.victory + value.victory,
acc.openArea + value.openArea,
)
}
result.copy(averageTime = result.totalTime / result.totalGames)
} else {
StatsModel(
title = 0,
totalGames = 0,
totalTime = 0,
averageTime = 0,
shortestTime = 0,
mines = 0,
victory = 0,
openArea = 0,
)
}
}
override fun initialState() = StatsState(
stats = listOf(),
showAds = !preferenceRepository.isPremiumEnabled()
)
override suspend fun mapEventToState(event: StatsEvent) = flow {
when (event) {
is StatsEvent.LoadStats -> {
emit(loadStatsModel())
emit(state.copy(stats = loadStatsModel()))
}
is StatsEvent.DeleteStats -> {
deleteAll()
emit(
state.copy(
totalGames = 0,
duration = 0,
averageDuration = 0,
mines = 0,
victory = 0,
openArea = 0,
)
)
emit(state.copy(stats = loadStatsModel()))
}
}
}

View file

@ -3,6 +3,8 @@ package dev.lucasnlm.antimine.support
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
@ -45,7 +47,16 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply {
setView(R.layout.dialog_payments)
val view = LayoutInflater
.from(context)
.inflate(R.layout.dialog_payments, null, false)
.apply {
findViewById<View>(R.id.close).setOnClickListener {
dismissAllowingStateLoss()
}
}
setView(view)
if (isInstantMode) {
setNeutralButton(R.string.no) { _, _ ->

View file

@ -61,9 +61,7 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
launch {
themeViewModel.observeState().collect {
if (usingTheme.id != it.current.id) {
finish()
startActivity(intent)
overridePendingTransition(0, 0)
recreate()
}
}
}

View file

@ -73,6 +73,7 @@ class TutorialViewModel(
fun openActionLabel(): String =
when (preferencesRepository.controlStyle()) {
ControlStyle.Standard -> context.getString(R.string.single_click)
ControlStyle.SwitchMarkOpen -> context.getString(R.string.single_click)
ControlStyle.FastFlag -> context.getString(R.string.long_press)
ControlStyle.DoubleClick -> context.getString(R.string.double_click)
ControlStyle.DoubleClickInverted -> context.getString(R.string.single_click)
@ -81,6 +82,7 @@ class TutorialViewModel(
fun flagActionLabel(): String =
when (preferencesRepository.controlStyle()) {
ControlStyle.Standard -> context.getString(R.string.long_press)
ControlStyle.SwitchMarkOpen -> context.getString(R.string.long_press)
ControlStyle.FastFlag -> context.getString(R.string.single_click)
ControlStyle.DoubleClick -> context.getString(R.string.single_click)
ControlStyle.DoubleClickInverted -> context.getString(R.string.double_click)
@ -207,6 +209,7 @@ class TutorialViewModel(
override suspend fun onSingleClick(index: Int) {
clock.stop()
when (preferencesRepository.controlStyle()) {
ControlStyle.SwitchMarkOpen -> openTileAction(index)
ControlStyle.Standard -> openTileAction(index)
ControlStyle.FastFlag -> longTileAction(index)
ControlStyle.DoubleClick -> longTileAction(index)
@ -217,6 +220,7 @@ class TutorialViewModel(
override suspend fun onLongClick(index: Int) {
clock.stop()
when (preferencesRepository.controlStyle()) {
ControlStyle.SwitchMarkOpen -> longTileAction(index)
ControlStyle.Standard -> longTileAction(index)
ControlStyle.FastFlag -> openTileAction(index)
else -> {}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59 7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L10.59,12 5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z" />
</vector>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="8dp"
android:left="8dp"
android:right="8dp"
android:top="8dp">
<shape android:shape="rectangle">
<stroke
android:width="1.5dp"
android:color="?attr/colorControlNormal" />
<corners android:radius="5dp" />
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8.79,9.24V5.5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v3.74c1.21,-0.81 2,-2.18 2,-3.74c0,-2.49 -2.01,-4.5 -4.5,-4.5s-4.5,2.01 -4.5,4.5C6.79,7.06 7.58,8.43 8.79,9.24zM14.29,11.71c-0.28,-0.14 -0.58,-0.21 -0.89,-0.21h-0.61v-6c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v10.74l-3.44,-0.72c-0.37,-0.08 -0.76,0.04 -1.03,0.31c-0.43,0.44 -0.43,1.14 0,1.58l4.01,4.01C9.71,21.79 10.22,22 10.75,22h6.1c1,0 1.84,-0.73 1.98,-1.72l0.63,-4.47c0.12,-0.85 -0.32,-1.69 -1.09,-2.07L14.29,11.71z"/>
</vector>

View file

@ -25,6 +25,19 @@
ads:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/menu" />
<ImageView
android:id="@+id/switchFlag"
android:layout_width="?attr/actionBarSize"
android:layout_height="?attr/actionBarSize"
android:background="@drawable/switch_control_border"
android:contentDescription="@string/switch_control"
android:clickable="true"
android:focusable="true"
android:padding="14dp"
ads:layout_constraintLeft_toRightOf="@id/menu"
ads:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/flag_black" />
<TextView
android:id="@+id/timer"
android:layout_width="wrap_content"
@ -42,7 +55,7 @@
android:visibility="gone"
ads:layout_constraintBottom_toBottomOf="@id/menu"
ads:layout_constraintHorizontal_chainStyle="packed"
ads:layout_constraintLeft_toLeftOf="parent"
ads:layout_constraintLeft_toRightOf="@id/switchFlag"
ads:layout_constraintRight_toLeftOf="@id/minesCount"
ads:layout_constraintTop_toTopOf="@id/menu"
tools:targetApi="m"
@ -68,7 +81,7 @@
ads:layout_constraintBottom_toBottomOf="@id/menu"
ads:layout_constraintHorizontal_chainStyle="packed"
ads:layout_constraintLeft_toRightOf="@id/timer"
ads:layout_constraintRight_toRightOf="parent"
ads:layout_constraintRight_toLeftOf="@id/shortcutIcon"
ads:layout_constraintTop_toTopOf="@id/menu"
tools:targetApi="m"
tools:text="99"

View file

@ -1,143 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
xmlns:app="http://schemas.android.com/apk/res-auto">
android:fitsSystemWindows="true">
<dev.lucasnlm.external.view.AdPlaceHolderView
android:id="@+id/ad_placeholder"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"/>
tools:visibility="visible" />
<TableLayout
android:id="@+id/stats"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:stretchColumns="1"
android:fitsSystemWindows="true"
android:divider="?android:listDivider"
android:showDividers="middle"
app:layout_constraintTop_toBottomOf="@id/ad_placeholder"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:context=".stats.StatsActivity">
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/ad_placeholder" />
<TableRow>
<TextView
android:padding="16dp"
android:textStyle="bold"
android:text="@string/games" />
<TextView
android:id="@+id/totalGames"
android:gravity="end"
android:padding="16dp"
android:text="0"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:textStyle="bold"
android:text="@string/mines" />
<TextView
android:id="@+id/minesCount"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:textStyle="bold"
android:text="@string/total_time" />
<TextView
android:id="@+id/totalTime"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:textStyle="bold"
android:text="@string/average_time" />
<TextView
android:id="@+id/averageTime"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:textStyle="bold"
android:text="@string/open_areas" />
<TextView
android:id="@+id/openAreas"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:textStyle="bold"
android:text="@string/performance" />
<TextView
android:id="@+id/performance"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:textStyle="bold"
android:text="@string/victories" />
<TextView
android:id="@+id/victory"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="16dp"
android:textStyle="bold"
android:text="@string/defeats" />
<TextView
android:id="@+id/defeat"
android:gravity="end"
android:padding="16dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
</TableLayout>
<TextView
android:id="@+id/empty"
android:text="@string/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -27,10 +27,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="18sp"
android:textStyle="bold"
android:textAlignment="center"
android:gravity="center_horizontal"
android:textAllCaps="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_emoji"
@ -42,7 +42,6 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/you_lost"
android:textSize="14sp"
android:textAlignment="center"
android:gravity="center_horizontal"
app:layout_constraintEnd_toEndOf="parent"
@ -56,7 +55,6 @@
android:layout_height="wrap_content"
android:text="@string/you_have_received"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textAlignment="center"
android:gravity="center"
app:drawableEndCompat="@drawable/tip"
@ -65,4 +63,15 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/subtitle"/>
<ImageView
android:id="@+id/close"
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/close"
android:padding="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,7 +4,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
android:padding="16dp">
<ImageView
android:id="@+id/emoji"
@ -24,7 +25,7 @@
android:text="@string/support_title"
android:layout_marginTop="24dp"
android:textStyle="bold"
android:textSize="18sp"
android:textAllCaps="true"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/emoji"/>
@ -38,10 +39,20 @@
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"/>
<ImageView
android:id="@+id/close"
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/close"
android:padding="16dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,34 @@
<?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:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_vertical"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:paddingStart="32dp"
android:paddingEnd="48dp"
android:paddingVertical="12dp">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@+id/standard_details"
app:layout_constraintEnd_toStartOf="@id/standard_details"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/firstAction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
tools:text="First Action" />
</LinearLayout>

View file

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:contentPadding="8dp"
app:cardCornerRadius="5dp"
app:cardPreventCornerOverlap="true"
app:cardElevation="7dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/statsLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAllCaps="true"
android:textStyle="bold"
android:textColor="@color/accent"
android:padding="8dp"
tools:text="General"/>
<TableLayout
android:id="@+id/stats"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:fitsSystemWindows="true"
android:divider="?android:listDivider"
android:showDividers="middle"
tools:context=".stats.StatsActivity">
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/games" />
<TextView
android:id="@+id/totalGames"
android:gravity="end"
android:padding="8dp"
android:text="0"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/mines" />
<TextView
android:id="@+id/minesCount"
android:gravity="end"
android:padding="8dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/total_time" />
<TextView
android:id="@+id/totalTime"
android:gravity="end"
android:padding="8dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/average_time" />
<TextView
android:id="@+id/averageTime"
android:gravity="end"
android:padding="8dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/shortest_time" />
<TextView
android:id="@+id/shortestTime"
android:gravity="end"
android:padding="8dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/open_areas" />
<TextView
android:id="@+id/openAreas"
android:gravity="end"
android:padding="8dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/performance" />
<TextView
android:id="@+id/performance"
android:gravity="end"
android:padding="8dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/victories" />
<TextView
android:id="@+id/victory"
android:gravity="end"
android:padding="8dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
<TableRow>
<TextView
android:padding="8dp"
android:textStyle="bold"
android:text="@string/defeats" />
<TextView
android:id="@+id/defeat"
android:gravity="end"
android:padding="8dp"
android:text="-"
tools:ignore="HardcodedText" />
</TableRow>
</TableLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>

View file

@ -73,6 +73,10 @@ class MockPreferencesRepository : IPreferencesRepository {
override fun setExtraTips(tips: Int) { }
override fun openUsingSwitchControl(): Boolean = true
override fun setSwitchControl(useOpen: Boolean) { }
override fun useFlagAssistant(): Boolean = false
override fun useHapticFeedback(): Boolean = true
@ -84,4 +88,6 @@ class MockPreferencesRepository : IPreferencesRepository {
override fun useQuestionMark(): Boolean = false
override fun isSoundEffectsEnabled(): Boolean = false
override fun showWindowsWhenFinishGame(): Boolean = true
}

View file

@ -2,6 +2,9 @@ package dev.lucasnlm.antimine.stats.viewmodel
import dev.lucasnlm.antimine.IntentViewModelTest
import dev.lucasnlm.antimine.common.level.database.models.Stats
import dev.lucasnlm.antimine.common.level.models.Minefield
import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.MemoryStatsRepository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import io.mockk.every
@ -14,145 +17,151 @@ import org.junit.Test
class StatsViewModelTest : IntentViewModelTest() {
private val listOfStats = listOf(
Stats(0, 1000, 10, 1, 10, 10, 90),
Stats(1, 1200, 24, 0, 10, 10, 20)
// Standard
Stats(0, 1000, 10, 1, 9, 12, 90),
Stats(1, 1200, 11, 0, 10, 10, 20),
// Expert
Stats(2, 2000, 99, 1, 24, 24, 90),
Stats(3, 3200, 99, 0, 24, 24, 20),
// Intermediate
Stats(4, 4000, 40, 1, 16, 16, 40),
Stats(5, 5200, 40, 0, 16, 16, 10),
// Beginner
Stats(6, 6000, 10, 1, 9, 9, 15),
Stats(7, 7200, 10, 0, 9, 9, 20),
// Custom
Stats(8, 8000, 5, 1, 5, 5, 5),
Stats(9, 9200, 5, 0, 5, 5, 4),
)
private val prefsRepository: IPreferencesRepository = mockk()
private val minefieldRepository: IMinefieldRepository = mockk()
private val dimensionRepository: IDimensionRepository = mockk()
@Before
override fun setup() {
super.setup()
every { prefsRepository.getStatsBase() } returns 0
every { prefsRepository.isPremiumEnabled() } returns false
every { minefieldRepository.fromDifficulty(any(), any(), any()) } returns Minefield(6, 12, 9)
}
@Test
fun testStatsTotalGames() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(2, statsModel.totalGames)
assertEquals(5, statsModel.stats.count())
}
@Test
fun testStatsTotalGamesWithBase() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
mapOf(0 to 2, 1 to 1, 2 to 0).forEach { (base, expected) ->
mapOf(0 to 5, 2 to 5, 4 to 4, 6 to 3, 8 to 2, 10 to 0).forEach { (base, expected) ->
every { prefsRepository.getStatsBase() } returns base
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModelBase0 = viewModel.singleState()
assertEquals(expected, statsModelBase0.totalGames)
assertEquals(expected, statsModelBase0.stats.size)
}
}
@Test
fun testStatsTotalGamesEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(0, statsModel.totalGames)
assertEquals(0, statsModel.stats.size)
}
@Test
fun testStatsDuration() = runBlockingTest {
fun testStatsTotalTime() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(2200L, statsModel.duration)
assertEquals(47000, statsModel.stats[0].totalTime)
assertEquals(5200, statsModel.stats[1].totalTime)
assertEquals(9200, statsModel.stats[2].totalTime)
assertEquals(13200, statsModel.stats[3].totalTime)
assertEquals(19400, statsModel.stats[4].totalTime)
}
@Test
fun testStatsDurationEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(0L, statsModel.duration)
}
@Test
fun testStatsAverageDuration() = runBlockingTest {
fun testStatsAverageTime() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(1100L, statsModel.averageDuration)
assertEquals(4700, statsModel.stats[0].averageTime)
assertEquals(2600, statsModel.stats[1].averageTime)
assertEquals(4600, statsModel.stats[2].averageTime)
assertEquals(6600, statsModel.stats[3].averageTime)
assertEquals(4850, statsModel.stats[4].averageTime)
}
@Test
fun testStatsAverageDurationEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
fun testStatsShortestTime() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(0L, statsModel.averageDuration)
assertEquals(1000, statsModel.stats[0].shortestTime)
assertEquals(2000, statsModel.stats[1].shortestTime)
assertEquals(4000, statsModel.stats[2].shortestTime)
assertEquals(6000, statsModel.stats[3].shortestTime)
assertEquals(1000, statsModel.stats[4].shortestTime)
}
@Test
fun testStatsMines() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(34, statsModel.mines)
}
@Test
fun testStatsMinesEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(0, statsModel.mines)
assertEquals(329, statsModel.stats[0].mines)
assertEquals(198, statsModel.stats[1].mines)
assertEquals(80, statsModel.stats[2].mines)
assertEquals(20, statsModel.stats[3].mines)
assertEquals(31, statsModel.stats[4].mines)
}
@Test
fun testVictory() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(1, statsModel.victory)
}
@Test
fun testVictoryEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(0, statsModel.victory)
assertEquals(5, statsModel.stats[0].victory)
assertEquals(1, statsModel.stats[1].victory)
assertEquals(1, statsModel.stats[2].victory)
assertEquals(1, statsModel.stats[3].victory)
assertEquals(2, statsModel.stats[4].victory)
}
@Test
fun testOpenArea() = runBlockingTest {
val repository = MemoryStatsRepository(listOfStats.toMutableList())
val viewModel = StatsViewModel(repository, prefsRepository)
val viewModel = StatsViewModel(repository, prefsRepository, minefieldRepository, dimensionRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(110, statsModel.openArea)
}
@Test
fun testOpenAreaEmpty() = runBlockingTest {
val repository = MemoryStatsRepository(mutableListOf())
val viewModel = StatsViewModel(repository, prefsRepository)
viewModel.sendEvent(StatsEvent.LoadStats)
val statsModel = viewModel.singleState()
assertEquals(0, statsModel.openArea)
assertEquals(314, statsModel.stats[0].openArea)
assertEquals(110, statsModel.stats[1].openArea)
assertEquals(50, statsModel.stats[2].openArea)
assertEquals(35, statsModel.stats[3].openArea)
assertEquals(119, statsModel.stats[4].openArea)
}
}

View file

@ -8,8 +8,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 801011
versionName '8.1.1'
versionCode 802001
versionName '8.2.0'
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -26,6 +26,7 @@ class GameController {
private var firstOpen: FirstOpen = FirstOpen.Unknown
private var gameControl: GameControl = GameControl.Standard
private var useQuestionMark = true
private var useOpenOnSwitchControl = true
val seed: Long
@ -117,6 +118,22 @@ class GameController {
this.actions++
minefieldHandler.openOrFlagNeighborsOf(target.id)
}
ActionResponse.OpenOrMark -> {
if (!hasMines()) {
if (target.mark.isNotNone()) {
minefieldHandler.removeMarkAt(target.id)
} else {
minefieldHandler.openAt(target.id, false)
}
} else {
this.actions++
if (useOpenOnSwitchControl) {
minefieldHandler.openAt(target.id, false)
} else {
minefieldHandler.switchMarkAt(target.id)
}
}
}
}
}
@ -307,4 +324,8 @@ class GameController {
fun useQuestionMark(useQuestionMark: Boolean) {
this.useQuestionMark = useQuestionMark
}
fun useOpenOnSwitchControl(useOpen: Boolean) {
this.useOpenOnSwitchControl = useOpen
}
}

View file

@ -350,6 +350,12 @@ open class GameViewModel(
}
}
fun refreshUseOpenOnSwitchControl(useOpen: Boolean) {
if (initialized) {
gameController.useOpenOnSwitchControl(useOpen)
}
}
fun runClock() {
clock.run {
if (isStopped) start {

View file

@ -8,6 +8,7 @@ enum class ActionResponse {
SwitchMark,
HighlightNeighbors,
OpenNeighbors,
OpenOrMark,
}
/**
@ -28,6 +29,7 @@ enum class ControlStyle {
DoubleClick,
FastFlag,
DoubleClickInverted,
SwitchMarkOpen
}
/**
@ -44,12 +46,12 @@ sealed class GameControl(
onCovered = Actions(
singleClick = ActionResponse.OpenTile,
longPress = ActionResponse.SwitchMark,
doubleClick = null
doubleClick = null,
),
onOpen = Actions(
singleClick = ActionResponse.HighlightNeighbors,
longPress = ActionResponse.OpenNeighbors,
doubleClick = null
doubleClick = null,
)
)
@ -58,12 +60,12 @@ sealed class GameControl(
onCovered = Actions(
singleClick = ActionResponse.SwitchMark,
longPress = ActionResponse.OpenTile,
doubleClick = null
doubleClick = null,
),
onOpen = Actions(
singleClick = ActionResponse.OpenNeighbors,
longPress = ActionResponse.HighlightNeighbors,
doubleClick = null
doubleClick = null,
)
)
@ -72,12 +74,12 @@ sealed class GameControl(
onCovered = Actions(
singleClick = ActionResponse.SwitchMark,
longPress = null,
doubleClick = ActionResponse.OpenTile
doubleClick = ActionResponse.OpenTile,
),
onOpen = Actions(
singleClick = ActionResponse.HighlightNeighbors,
longPress = null,
doubleClick = ActionResponse.OpenNeighbors
doubleClick = ActionResponse.OpenNeighbors,
)
)
@ -86,12 +88,26 @@ sealed class GameControl(
onCovered = Actions(
singleClick = ActionResponse.OpenTile,
longPress = null,
doubleClick = ActionResponse.SwitchMark
doubleClick = ActionResponse.SwitchMark,
),
onOpen = Actions(
singleClick = ActionResponse.HighlightNeighbors,
longPress = null,
doubleClick = ActionResponse.OpenNeighbors
doubleClick = ActionResponse.OpenNeighbors,
)
)
object SwitchMarkOpen : GameControl(
id = ControlStyle.SwitchMarkOpen,
onCovered = Actions(
singleClick = ActionResponse.OpenOrMark,
longPress = null,
doubleClick = null,
),
onOpen = Actions(
singleClick = ActionResponse.HighlightNeighbors,
longPress = null,
doubleClick = null,
)
)
@ -102,6 +118,7 @@ sealed class GameControl(
ControlStyle.DoubleClick -> DoubleClick
ControlStyle.FastFlag -> FastFlag
ControlStyle.DoubleClickInverted -> DoubleClickInverted
ControlStyle.SwitchMarkOpen -> SwitchMarkOpen
}
}
}

View file

@ -51,10 +51,14 @@ interface IPreferencesRepository {
fun getExtraTips(): Int
fun setExtraTips(tips: Int)
fun openUsingSwitchControl(): Boolean
fun setSwitchControl(useOpen: Boolean)
fun useFlagAssistant(): Boolean
fun useHapticFeedback(): Boolean
fun areaSizeMultiplier(): Int
fun useAnimations(): Boolean
fun useQuestionMark(): Boolean
fun isSoundEffectsEnabled(): Boolean
fun showWindowsWhenFinishGame(): Boolean
}

View file

@ -63,6 +63,9 @@ class PreferencesRepository(
override fun isSoundEffectsEnabled(): Boolean =
preferencesManager.getBoolean(PREFERENCE_SOUND_EFFECTS, false)
override fun showWindowsWhenFinishGame(): Boolean =
preferencesManager.getBoolean(PREFERENCE_SHOW_WINDOWS, true)
override fun controlStyle(): ControlStyle {
val index = preferencesManager.getInt(PREFERENCE_CONTROL_STYLE, -1)
return ControlStyle.values().getOrNull(index) ?: ControlStyle.Standard
@ -212,6 +215,14 @@ class PreferencesRepository(
preferencesManager.putInt(PREFERENCE_EXTRA_TIPS, tips)
}
override fun openUsingSwitchControl(): Boolean {
return preferencesManager.getBoolean(PREFERENCE_USE_OPEN_SWITCH_CONTROL, true)
}
override fun setSwitchControl(useOpen: Boolean) {
preferencesManager.putBoolean(PREFERENCE_USE_OPEN_SWITCH_CONTROL, useOpen)
}
private companion object {
private const val PREFERENCE_VIBRATION = "preference_vibration"
private const val PREFERENCE_ASSISTANT = "preference_assistant"
@ -239,5 +250,7 @@ class PreferencesRepository(
private const val PREFERENCE_SHOW_SUPPORT = "preference_show_support"
private const val PREFERENCE_TIPS = "preference_current_tips"
private const val PREFERENCE_EXTRA_TIPS = "preference_extra_tips"
private const val PREFERENCE_SHOW_WINDOWS = "preference_show_windows"
private const val PREFERENCE_USE_OPEN_SWITCH_CONTROL = "preference_use_open_switch_control"
}
}

View file

@ -4,7 +4,7 @@
<color name="primary">#212121</color>
<color name="primary_dark">#212121</color>
<color name="background">#00212121</color>
<color name="background">#FF212121</color>
<color name="mines_around_1">#d5d2cc</color>
<color name="mines_around_2">#d5d2cc</color>

View file

@ -4,7 +4,7 @@
<color name="primary">#FFFFFF</color>
<color name="primary_dark">#9E9E9E</color>
<color name="background">#00FFFFFF</color>
<color name="background">#FFFFFFFF</color>
<color name="mines_around_1">#527F8D</color>
<color name="mines_around_2">#2B8D43</color>

View file

@ -65,6 +65,7 @@
<string name="open_areas">Open Areas</string>
<string name="total_time">Total Time</string>
<string name="average_time">Average Time</string>
<string name="shortest_time">Shortest Time</string>
<string name="performance">Performance</string>
<string name="ok">OK</string>
<string name="use_question_mark">Use Question Mark</string>
@ -120,6 +121,9 @@
<string name="flag_removed">Flag removed!</string>
<string name="remove_ad">Remove Ads</string>
<string name="help">Help</string>
<string name="show_windows">Show windows</string>
<string name="switch_control">Switch: Flag and Open</string>
<string name="switch_control_desc">Use button to switch between Flag and Open</string>
<string name="app_description">You have to clear a rectangular board containing hidden mines without detonating any of them.</string>
<string name="app_name">Antimine</string>
</resources>

View file

@ -30,32 +30,6 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_general"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
android:checked="true"
android:defaultValue="true"
android:key="preference_vibration"
android:title="@string/vibration"
app:iconSpaceReserved="false"/>
<SwitchPreferenceCompat
android:checked="false"
android:defaultValue="false"
android:key="preference_sound"
android:title="@string/sound_effects"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:checked="true"
android:defaultValue="true"
android:key="preference_animation"
android:title="@string/animations"
app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_accessibility"
app:iconSpaceReserved="false">
@ -98,4 +72,37 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/settings_general"
app:iconSpaceReserved="false">
<SwitchPreferenceCompat
android:checked="true"
android:defaultValue="true"
android:key="preference_vibration"
android:title="@string/vibration"
app:iconSpaceReserved="false"/>
<SwitchPreferenceCompat
android:checked="false"
android:defaultValue="false"
android:key="preference_sound"
android:title="@string/sound_effects"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:checked="true"
android:defaultValue="true"
android:key="preference_animation"
android:title="@string/animations"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:checked="true"
android:defaultValue="true"
android:key="preference_show_windows"
android:title="@string/show_windows"
app:iconSpaceReserved="false" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -6,8 +6,8 @@ android {
compileSdkVersion 30
defaultConfig {
versionCode 801011
versionName '8.1.1'
versionCode 802001
versionName '8.2.0'
minSdkVersion 21
targetSdkVersion 30
}

View file

@ -10,8 +10,8 @@ android {
compileSdkVersion 30
defaultConfig {
versionCode 801011
versionName '8.1.1'
versionCode 802001
versionName '8.2.0'
minSdkVersion 21
targetSdkVersion 30
}

View file

@ -8,8 +8,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 801011
versionName '8.1.1'
versionCode 802001
versionName '8.2.0'
applicationId 'dev.lucasnlm.antimine'
minSdkVersion 23
targetSdkVersion 30