Merge pull request #175 from lucasnlm/small-improvements
Small improvements
This commit is contained in:
commit
5a60c4ba90
39 changed files with 904 additions and 365 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) { _, _ ->
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package dev.lucasnlm.antimine.stats.model
|
||||
|
||||
data class StatsState(
|
||||
val stats: List<StatsModel>,
|
||||
val showAds: Boolean
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) { _, _ ->
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 -> {}
|
||||
|
|
10
app/src/main/res/drawable/close.xml
Normal file
10
app/src/main/res/drawable/close.xml
Normal 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>
|
17
app/src/main/res/drawable/switch_control_border.xml
Normal file
17
app/src/main/res/drawable/switch_control_border.xml
Normal 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>
|
10
app/src/main/res/drawable/touch.xml
Normal file
10
app/src/main/res/drawable/touch.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
34
app/src/main/res/layout/view_control_item_simple.xml
Normal file
34
app/src/main/res/layout/view_control_item_simple.xml
Normal 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>
|
171
app/src/main/res/layout/view_stats.xml
Normal file
171
app/src/main/res/layout/view_stats.xml
Normal 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>
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -350,6 +350,12 @@ open class GameViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun refreshUseOpenOnSwitchControl(useOpen: Boolean) {
|
||||
if (initialized) {
|
||||
gameController.useOpenOnSwitchControl(useOpen)
|
||||
}
|
||||
}
|
||||
|
||||
fun runClock() {
|
||||
clock.run {
|
||||
if (isStopped) start {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -6,8 +6,8 @@ android {
|
|||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
versionCode 801011
|
||||
versionName '8.1.1'
|
||||
versionCode 802001
|
||||
versionName '8.2.0'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ android {
|
|||
compileSdkVersion 30
|
||||
|
||||
defaultConfig {
|
||||
versionCode 801011
|
||||
versionName '8.1.1'
|
||||
versionCode 802001
|
||||
versionName '8.2.0'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue