Merge pull request #220 from lucasnlm/add-recycler-padding

Add recycler padding
This commit is contained in:
Lucas Nunes 2020-12-17 11:07:38 -03:00 committed by GitHub
commit 0d6c6cfd4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 198 additions and 93 deletions

View file

@ -15,8 +15,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 805011
versionName '8.5.1'
versionCode 805021
versionName '8.5.2'
minSdkVersion 21
targetSdkVersion 30
multiDexEnabled true

View file

@ -10,6 +10,7 @@ import android.view.Gravity
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
@ -68,6 +69,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.singleOrNull
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.lang.Exception
@ -121,6 +123,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
bindNavigationMenu()
bindSwitchControlButton()
bindAds()
bindPrice()
findViewById<FrameLayout>(R.id.levelContainer).doOnLayout {
if (!isFinishing) {
@ -488,14 +491,11 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
findItem(R.id.share_now).isVisible = isNotInstant
findItem(R.id.remove_ads).isVisible = !preferencesRepository.isPremiumEnabled() && isNotInstant
findItem(R.id.previous_games).isVisible = featureFlagManager.isGameHistoryEnabled()
findItem(R.id.rate).isVisible = featureFlagManager.isRateUsEnabled()
if (!playGamesManager.hasGooglePlayGames()) {
removeGroup(R.id.play_games_group)
}
// New Features
findItem(R.id.themes).setActionView(R.layout.new_icon)
findItem(R.id.tutorial).setActionView(R.layout.new_icon)
}
}
@ -899,6 +899,36 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
}
private fun bindPrice() {
if (billingManager.isEnabled() && !preferencesRepository.isPremiumEnabled()) {
billingManager.start()
lifecycleScope.launchWhenResumed {
billingManager.getPrice().collect { price ->
if (price.isNotBlank()) {
try {
navigationView.menu.findItem(R.id.remove_ads).apply {
actionView = TextView(baseContext).apply {
text = price
gravity = Gravity.CENTER_VERTICAL
setTextColor(ContextCompat.getColor(context, R.color.mines_around_2))
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
).apply {
gravity = Gravity.CENTER
}
}
}
} catch (e: Exception) {
Log.e(TAG, "Fail to create price text")
}
}
}
}
}
}
private fun refreshAds() {
val isTutorialComplete = preferencesRepository.isTutorialCompleted()
if (isTutorialComplete && !preferencesRepository.isPremiumEnabled() && billingManager.isEnabled()) {
@ -946,12 +976,16 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null &&
!instantAppManager.isEnabled(this)
) {
if (billingManager.isEnabled()) {
SupportAppDialogFragment.newRemoveAdsSupportDialog()
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
} else {
SupportAppDialogFragment.newRequestSupportDialog()
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
lifecycleScope.launch {
if (billingManager.isEnabled()) {
SupportAppDialogFragment.newRemoveAdsSupportDialog(
applicationContext,
billingManager.getPrice().singleOrNull()
).show(supportFragmentManager, SupportAppDialogFragment.TAG)
} else {
SupportAppDialogFragment.newRequestSupportDialog(applicationContext)
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}
}
}

View file

@ -53,7 +53,7 @@ class AboutActivity : ThematicActivity(R.layout.activity_empty) {
private fun showSupportAppDialog() {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment.newRequestSupportDialog()
SupportAppDialogFragment.newRequestSupportDialog(applicationContext)
.show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}

View file

@ -10,6 +10,7 @@ import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.about.viewmodel.AboutEvent
import dev.lucasnlm.antimine.about.viewmodel.AboutViewModel
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.external.IBillingManager
import kotlinx.android.synthetic.main.fragment_about_info.*
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@ -17,6 +18,7 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class AboutInfoFragment : Fragment(R.layout.fragment_about_info) {
private val aboutViewModel: AboutViewModel by sharedViewModel()
private val preferencesRepository: IPreferencesRepository by inject()
private val billingManager: IBillingManager by inject()
override fun onResume() {
super.onResume()
@ -38,12 +40,12 @@ class AboutInfoFragment : Fragment(R.layout.fragment_about_info) {
}
}
if (preferencesRepository.showSupport()) {
if (preferencesRepository.isPremiumEnabled() && billingManager.isEnabled()) {
supportUs.visibility = View.GONE
} else {
supportUs.setOnClickListener {
aboutViewModel.sendEvent(AboutEvent.SupportUs)
}
} else {
supportUs.visibility = View.GONE
}
thirdsParties.setOnClickListener {

View file

@ -67,6 +67,19 @@ open class LevelFragment : CommonLevelFragment(R.layout.fragment_level) {
eventObserver.observe(
viewLifecycleOwner,
{
if (!gameViewModel.hasPlantedMines()) {
recyclerGrid.post {
levelSetup.value?.let { minefield ->
val size = dimensionRepository.areaSizeWithPadding()
val actionBarSize = dimensionRepository.actionBarSize()
val dx = minefield.width * size * 0.25f
val dy = (minefield.height * size + actionBarSize) * 0.25f - size * 3
recyclerGrid.smoothScrollBy(dx.toInt(), dy.toInt(), null, 200)
}
}
}
when (it) {
Event.Pause,
Event.GameOver,

View file

@ -2,6 +2,7 @@ package dev.lucasnlm.antimine.splash.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.lucasnlm.antimine.common.level.database.models.Stats
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.core.control.ControlStyle
@ -9,6 +10,7 @@ import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.support.IapHandler
import dev.lucasnlm.external.ICloudStorageManager
import dev.lucasnlm.external.model.CloudSave
import kotlinx.coroutines.launch
class SplashViewModel(
private val preferencesRepository: IPreferencesRepository,
@ -17,7 +19,9 @@ class SplashViewModel(
private val iapHandler: IapHandler,
) : ViewModel() {
fun startIap() {
iapHandler.start()
viewModelScope.launch {
iapHandler.start()
}
}
suspend fun migrateCloudSave(playGamesId: String) {

View file

@ -7,9 +7,7 @@ import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.external.IBillingManager
import dev.lucasnlm.external.model.PurchaseInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class IapHandler(
@ -17,14 +15,12 @@ class IapHandler(
private val preferencesManager: IPreferencesRepository,
private val billingManager: IBillingManager,
) {
fun start() {
GlobalScope.launch {
billingManager.listenPurchases().collect {
if (it is PurchaseInfo.PurchaseResult) {
onLockStatusChanged(it.unlockStatus)
} else {
showFailToConnectFeedback()
}
suspend fun start() {
billingManager.listenPurchases().collect {
if (it is PurchaseInfo.PurchaseResult) {
onLockStatusChanged(it.unlockStatus)
} else {
showFailToConnectFeedback()
}
}
}

View file

@ -1,6 +1,8 @@
package dev.lucasnlm.antimine.support
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
@ -27,7 +29,7 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
private val adsManager: IAdsManager by inject()
private val instantAppManager: InstantAppManager by inject()
private var unlockMessage: Int = R.string.support_action
private lateinit var unlockMessage: String
private var targetThemeId: Long = -1L
private var isInstantMode: Boolean = true
@ -40,11 +42,13 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
analyticsManager.sentEvent(Analytics.ShowIapDialog)
unlockMessage =
(arguments?.getInt(UNLOCK_LABEL) ?: savedInstanceState?.getInt(UNLOCK_LABEL)) ?: R.string.support_action
(arguments?.getString(UNLOCK_LABEL) ?: savedInstanceState?.getString(UNLOCK_LABEL))
?: getString(R.string.support_action)
targetThemeId =
(arguments?.getLong(TARGET_THEME_ID, -1L)) ?: -1L
}
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext()).apply {
val view = LayoutInflater
@ -148,26 +152,30 @@ class SupportAppDialogFragment : AppCompatDialogFragment() {
private const val UNLOCK_LABEL = "support_unlock_label"
private const val TARGET_THEME_ID = "target_theme_id"
fun newRequestSupportDialog(): SupportAppDialogFragment {
fun newRequestSupportDialog(context: Context): SupportAppDialogFragment {
return SupportAppDialogFragment().apply {
arguments = Bundle().apply {
putInt(UNLOCK_LABEL, R.string.support_action)
putString(UNLOCK_LABEL, context.getString(R.string.support_action))
}
}
}
fun newRemoveAdsSupportDialog(): SupportAppDialogFragment {
fun newRemoveAdsSupportDialog(context: Context, price: String?): SupportAppDialogFragment {
return SupportAppDialogFragment().apply {
val label = context.getString(R.string.remove_ad)
val unlockLabel = price?.let { "$label - $it" } ?: label
arguments = Bundle().apply {
putInt(UNLOCK_LABEL, R.string.remove_ad)
putString(UNLOCK_LABEL, unlockLabel)
}
}
}
fun newChangeThemeDialog(targetThemeId: Long): SupportAppDialogFragment {
fun newChangeThemeDialog(context: Context, targetThemeId: Long, price: String?): SupportAppDialogFragment {
return SupportAppDialogFragment().apply {
val label = context.getString(R.string.unlock_all)
val unlockLabel = price?.let { "$label - $it" } ?: label
arguments = Bundle().apply {
putInt(UNLOCK_LABEL, R.string.unlock_all)
putString(UNLOCK_LABEL, unlockLabel)
putLong(TARGET_THEME_ID, targetThemeId)
}
}

View file

@ -15,8 +15,11 @@ import dev.lucasnlm.antimine.support.SupportAppDialogFragment
import dev.lucasnlm.antimine.theme.view.ThemeAdapter
import dev.lucasnlm.antimine.theme.viewmodel.ThemeEvent
import dev.lucasnlm.antimine.theme.viewmodel.ThemeViewModel
import dev.lucasnlm.external.IBillingManager
import kotlinx.android.synthetic.main.activity_game.*
import kotlinx.android.synthetic.main.activity_theme.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -30,6 +33,8 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
private val preferencesRepository: IPreferencesRepository by inject()
private val billingManager: IBillingManager by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated {
@ -90,8 +95,14 @@ class ThemeActivity : ThematicActivity(R.layout.activity_theme) {
private fun showUnlockDialog(themeId: Long) {
if (supportFragmentManager.findFragmentByTag(SupportAppDialogFragment.TAG) == null) {
SupportAppDialogFragment.newChangeThemeDialog(themeId).apply {
show(supportFragmentManager, SupportAppDialogFragment.TAG)
lifecycleScope.launch {
SupportAppDialogFragment.newChangeThemeDialog(
applicationContext,
themeId,
billingManager.getPrice().singleOrNull()
).apply {
show(supportFragmentManager, SupportAppDialogFragment.TAG)
}
}
}
}

View file

@ -32,6 +32,8 @@ val AppModule = module {
override suspend fun charge(activity: Activity) { }
override fun getPrice(): Flow<String> = flowOf()
override fun listenPurchases(): Flow<PurchaseInfo> = flowOf()
}
} bind IBillingManager::class

View file

@ -10,8 +10,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 805011
versionName '8.5.1'
versionCode 805021
versionName '8.5.2'
minSdkVersion 21
targetSdkVersion 30
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View file

@ -17,8 +17,8 @@ import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import kotlin.math.nextDown
abstract class CommonLevelFragment(@LayoutRes val contentLayoutId: Int) : Fragment(contentLayoutId) {
private val dimensionRepository: IDimensionRepository by inject()
private val preferencesRepository: IPreferencesRepository by inject()
protected val dimensionRepository: IDimensionRepository by inject()
protected val gameViewModel by sharedViewModel<GameViewModel>()
protected val areaAdapter by lazy {
AreaAdapter(requireContext(), gameViewModel, preferencesRepository, dimensionRepository)
@ -39,7 +39,12 @@ abstract class CommonLevelFragment(@LayoutRes val contentLayoutId: Int) : Fragme
recyclerGrid.apply {
val horizontalPadding = calcHorizontalPadding(view, levelSetup.width)
val verticalPadding = calcVerticalPadding(view, levelSetup.height)
setPadding(horizontalPadding, verticalPadding, 0, 0)
if (horizontalPadding == 0 && verticalPadding == 0) {
val minPadding = dimensionRepository.areaSize().toInt()
setPadding(minPadding, minPadding, minPadding, minPadding)
} else {
setPadding(horizontalPadding, verticalPadding, 0, 0)
}
layoutManager = makeNewLayoutManager(levelSetup.width)
adapter = areaAdapter
}

View file

@ -384,6 +384,10 @@ open class GameViewModel(
gameController.revealAllEmptyAreas()
}
fun hasPlantedMines(): Boolean {
return gameController.mines().isNotEmpty()
}
fun revealRandomMine(): Boolean {
return if (gameController.revealRandomMine()) {
if (tipRepository.removeTip()) {

View file

@ -565,7 +565,7 @@ class FixedGridLayoutManager : RecyclerView.LayoutManager() {
// Check right bound
if (rightBoundReached) {
// If we've reached the last column, enforce limits
val rightOffset = horizontalSpace - getDecoratedRight(bottomView) + paddingRight
val rightOffset = horizontalSpace - getDecoratedRight(bottomView) - paddingRight
(-dx).coerceAtLeast(rightOffset)
} else {
// No limits while the last column isn't visible
@ -648,7 +648,7 @@ class FixedGridLayoutManager : RecyclerView.LayoutManager() {
val bottomOffset: Int =
if (rowOfIndex(childCount - 1) >= maxRowCount - 1) {
// We are truly at the bottom, determine how far
verticalSpace - getDecoratedBottom(bottomView) +
verticalSpace - getDecoratedBottom(bottomView) -
paddingBottom
} else {
/*

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:locale="fr">
<string name="tutorial">Tutoriel</string>
<string name="tutorial_completed">Tutoriel Terminé</string>
<string name="tutorial">Didacticiel</string>
<string name="tutorial_completed">Didacticiel terminé</string>
<string name="tutorial_0_bottom">Vous pouvez commencer par %1$s à n\'importe quel carré aléatoire.</string>
<string name="tutorial_1_top">Les chiffres indiquent combien de mines sont adjacentes au carré surligné.</string>
<string name="tutorial_1_bottom">Par conséquent, si le coin 1⃣ n\'a qu\'un carré adjacent, il doit s\'agir d\'une mine. %1$s pour le signaler.</string>
@ -9,7 +9,7 @@
<string name="tutorial_2_bottom">Vous pouvez l\'ouvrir par %1$s.</string>
<string name="tutorial_3_top">Il en va de même pour 1⃣ . Vous savez où se trouve la mine adjacente.</string>
<string name="tutorial_3_bottom">Vous pouvez ouvrir tous les autres carrés non ouverts adjacents à 1⃣ .</string>
<string name="tutorial_4_top">Nous savons où se trouve l\'une des mines, donc il n\'y a qu\'une seule possibilité pour l\'autre.</string>
<string name="tutorial_4_top">Nous savons où se trouve l\'une des mines. Il n\'y a donc qu\'une seule possibilité pour l\'autre.</string>
<string name="tutorial_4_bottom">%1$s pour marquer la mine adjacente à 2⃣ .</string>
<string name="tutorial_5_top">Vérifier le nombre en surbrillance.</string>
<string name="tutorial_5_bottom">%1$s pour ouvrir ou %2$s pour marquer.</string>
@ -20,7 +20,7 @@
<string name="beginner">Facile</string>
<string name="intermediate">Intermédiaire</string>
<string name="expert">Difficile</string>
<string name="support_description">Avec votre aide, nous serons en mesure de implémenter de nouvelles fonctionnalités et de maintenir notre projet actif.</string>
<string name="support_description">Avec votre aide, nous serons en mesure de mettre en place de nouvelles fonctionnalités et de maintenir notre projet actif.</string>
<string name="open">Ouvrir</string>
<string name="settings">Réglages</string>
<string name="animations">Animations</string>
@ -81,9 +81,9 @@
<string name="retry">Réessayer</string>
<string name="continue_game">Continuer</string>
<string name="empty">Vide</string>
<string name="cant_do_it_now">Impossible de faire cela maintenant</string>
<string name="cant_do_it_now">Impossible de faire ça maintenant</string>
<string name="you_have_received">Vous avez reçu : +%1$d</string>
<string name="help_win_a_game">Pour plus d\'Aide, vous devez gagner une partie.</string>
<string name="help_win_a_game">Pour plus d\'aide, vous devez gagner une partie.</string>
<string name="unknown_error">Erreur inconnue.</string>
<string name="leaderboards">Classements</string>
<string name="cancel">Annuler</string>

View file

@ -54,7 +54,7 @@
<string name="you_won">Du vant!</string>
<string name="victories">Seire</string>
<string name="you_lost">Du tapte!</string>
<string name="you_finished">You finished!</string>
<string name="you_finished">Du er ferdig!</string>
<string name="defeats">Nederlag</string>
<string name="game_over_desc_1">Lykke til på det neste spillet.</string>
<string name="game_over_desc_4">Du har avjobbet minefeltet i %1$d sekunder.</string>
@ -79,7 +79,7 @@
<string name="open_tile">Åpne</string>
<string name="flag_tile">Flagge</string>
<string name="retry">Prøv på nytt</string>
<string name="continue_game">Continue</string>
<string name="continue_game">Fortsett</string>
<string name="empty">Tom</string>
<string name="cant_do_it_now">Umulig å gjøre det nå</string>
<string name="you_have_received">Du har mottatt: +%1$d</string>

View file

@ -4,7 +4,7 @@
<string name="tutorial_completed">Tutorial Concluído</string>
<string name="tutorial_0_bottom">Para começar dê um %1$s em qualquer quadrado aleatório.</string>
<string name="tutorial_1_top">Os números indicam quantas minas existem adjacentes ao quadrado destacado.</string>
<string name="tutorial_1_bottom">Portanto, se o canto 1⃣ tem apenas um quadrado adjacente, ele deve ter uma minha. Use %1$s para sinalizá-lo.</string>
<string name="tutorial_1_bottom">Portanto, se o canto 1⃣ tem apenas um quadrado adjacente, ele deve ter uma mina. Use %1$s para sinalizá-lo.</string>
<string name="tutorial_2_top">O próximo quadrado fechado não tem uma mina porque o número é 1⃣ , não 2⃣ .</string>
<string name="tutorial_2_bottom">Você pode abri-lo com %1$s.</string>
<string name="tutorial_3_top">O mesmo acontece nesse 1⃣ . Você sabe onde está a mina adjacente.</string>

View file

@ -8,5 +8,6 @@ interface IBillingManager {
fun start()
fun isEnabled(): Boolean
suspend fun charge(activity: Activity)
fun getPrice(): Flow<String>
fun listenPurchases(): Flow<PurchaseInfo>
}

View file

@ -2,4 +2,5 @@ package dev.lucasnlm.external
interface IFeatureFlagManager {
fun isGameHistoryEnabled(): Boolean
fun isRateUsEnabled(): Boolean
}

View file

@ -1,5 +1,5 @@
Testez votre puissance de raisonnement sur un jeu de puzzle difficile où vous devez résoudre un champ de mines sans exploser aucune mine.
Il s'agit d'une version minimaliste et moderne du Minesweeper classique.
Testez votre puissance de raisonnement dans un jeu de puzzle difficile où vous devez nettoyer un champ de mines sans faire exploser aucune mine.
Il s'agit d'une version minimaliste et moderne du Démineur classique.
Êtes-vous en mesure de nettoyer un champ de mines ? C'est parti !
@ -7,50 +7,50 @@ OBJECTIF
C'est comme le Démineur. Vous gagnez la partie quand vous posez un drapeau sur chaque mine sur le champ de mine. Attention de ne pas en activer !
PAS BESOIN DE DEVINER
Ce jeu a un algorithme qui crée toujours des champs de mines solvables. Vous n'aurez pas à deviner où se trouve la dernière mine !
Ce jeu a un algorithme qui crée toujours des champs de mines résolubles. Vous n'aurez pas à deviner où se trouve la dernière mine !
NIVEAUX DE JEU
Tout comme Minesweeper, ce jeu a différents niveaux: débutant, intermédiaire et expert. Mais il a aussi un mode progressif unique qui devient plus difficile quand vous gagnez.
Tout comme le Démineur, ce jeu a différents niveaux : débutant, intermédiaire et expert. Mais il a aussi un mode progressif unique qui devient plus difficile quand vous gagnez.
NIVEAU PERSONNALISÉ
Si les modes standards ne suffisent pas, vous pouvez créer votre niveau de difficulté !
THÉMES
Antimine a plus de 15 thèmes (lumière, sombre et AMOLED). Et bientôt d'autres seront ajoutés!
THÈMES
Antimine a plus de 15 thèmes (incluant la lumière, l'obscurité et l'AMOLED). Et bientôt d'autres seront ajoutés !
MINIMALISTE
Une apparence et une sensation de netteté : une taille visuelle minimaliste et aussi légère (moins de 9 Mo).
TUTORIEL
Vous ne savez pas jouer au Démineur ? Pas de problème, vous pouvez jouer notre niveau de tutoriel et activer les conseils dans les paramètres.
DIDACTICIEL
Vous ne savez pas jouer au Démineur ? Pas de problème, vous pouvez jouer à notre niveau de didacticiel et activer les conseils dans les paramètres.
STATISTIQUES
Vérifiez vos résultats et votre développement.
Vérifiez vos résultats et votre progression.
ASSISTANT DU JEU
Le jeu marquera automatiquement les carrés que vous avez isolés.
PERSONNALISATION
Vous pouvez changer le jeu pour le faire comme vous le souhaitez.
Exemple: aide, animations, retours haptiques, point d'interrogation.
Vous pouvez modifier le jeu pour en faire ce que vous souhaitez.
Exemple : aide, animations, retour tactile, point d'interrogation.
CONTRÔLES
Il supporte quatre systèmes de contrôle différents. Exemple: drapeau en appuyant sur appui ou appui long.
Il supporte quatre systèmes de contrôle différents. Exemple : drapeau en cliquant ou avec un clic prolongé.
ACHIEVEMENTS
Vous avez huit réalisations à débloquer et différents classements à partager avec vos amis.
SUCCÈS ET CLASSEMENTS
Vous avez huit succès à débloquer et différents classements à partager avec vos amis.
ACCESSIBILITÉ
Vous pouvez personnaliser ce jeu en fonction de vos besoins: changer la taille des carrés, le rayon, le minutage tactile et le support du lecteur d'écran.
Vous pouvez personnaliser ce jeu en fonction de vos besoins : changer la taille des carrés, leur rayon, le minutage tactile et le support du lecteur d'écran.
Nous essayons toujours d'améliorer nos paramètres d'accessibilité. Si vous manquez quelque chose, faites-nous part de vos commentaires !
ANDROID
Il prend en charge les fonctionnalités cool d'Android comme l'écran partagé, les raccourcis, le partage, le thème du système et toujours le support de la dernière version.
Il prend en charge les fonctionnalités cool d'Android comme l'écran partagé, les raccourcis, le partage, le thème du système et supporte toujours la dernière version.
EN LIGNE OU HORS LIGNE
Vous n'avez pas besoin d'être en ligne pour jouer au jeu!
Vous n'avez pas besoin d'être en ligne pour jouer au jeu !
SAUVEGARDE AUTO
Vous pouvez commencer une partie et continuer plus tard! Il sauvegarder automatiquement votre partie.
SAUVEGARDE AUTOMATIQUE
Vous pouvez commencer une partie et la continuer plus tard ! Il sauvegardera automatiquement votre partie.
Profitez-en !

View file

@ -8,8 +8,8 @@ android {
compileSdkVersion 30
defaultConfig {
versionCode 805011
versionName '8.5.1'
versionCode 805021
versionName '8.5.2'
minSdkVersion 21
targetSdkVersion 30
}

View file

@ -23,6 +23,8 @@ class BillingManager(
activity.startActivity(intent)
}
override fun getPrice(): Flow<String> = flowOf()
override fun listenPurchases(): Flow<PurchaseInfo> = flowOf()
companion object {

View file

@ -2,4 +2,5 @@ package dev.lucasnlm.external
class FeatureFlagManager : IFeatureFlagManager {
override fun isGameHistoryEnabled(): Boolean = true
override fun isRateUsEnabled(): Boolean = false
}

View file

@ -12,8 +12,8 @@ android {
compileSdkVersion 30
defaultConfig {
versionCode 805011
versionName '8.5.1'
versionCode 805021
versionName '8.5.2'
minSdkVersion 21
targetSdkVersion 30
}

View file

@ -14,7 +14,10 @@ import com.android.billingclient.api.querySkuDetails
import dev.lucasnlm.external.model.PurchaseInfo
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.filterNotNull
class BillingManager(
private val context: Context,
@ -22,6 +25,8 @@ class BillingManager(
private val purchaseBroadcaster = ConflatedBroadcastChannel<PurchaseInfo>()
private var unlockPrice = MutableStateFlow<String?>(null)
private val billingClient by lazy {
BillingClient.newBuilder(context)
.setListener(this)
@ -29,6 +34,8 @@ class BillingManager(
.build()
}
override fun getPrice(): Flow<String> = unlockPrice.asSharedFlow().filterNotNull()
override fun listenPurchases(): Flow<PurchaseInfo> = purchaseBroadcaster.asFlow()
private fun handlePurchases(purchases: List<Purchase>) {
@ -61,17 +68,23 @@ class BillingManager(
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
val purchasesList: List<Purchase> = if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(listOf(BASIC_SUPPORT))
.setType(BillingClient.SkuType.INAPP)
.build()
billingClient.queryPurchases(BillingClient.SkuType.INAPP).purchasesList.let {
it?.toList() ?: listOf()
}
} else {
listOf()
billingClient
.querySkuDetailsAsync(skuDetailsParams) { _, list ->
unlockPrice.tryEmit(list?.firstOrNull()?.price)
}
val purchasesList: List<Purchase> = billingClient
.queryPurchases(BillingClient.SkuType.INAPP)
.purchasesList.let { it?.toList() ?: listOf() }
handlePurchases(purchasesList)
}
handlePurchases(purchasesList)
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
@ -94,12 +107,12 @@ class BillingManager(
}
override suspend fun charge(activity: Activity) {
val skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(listOf(BASIC_SUPPORT))
.setType(BillingClient.SkuType.INAPP)
.build()
if (billingClient.isReady) {
val skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(listOf(BASIC_SUPPORT))
.setType(BillingClient.SkuType.INAPP)
.build()
val details = billingClient.querySkuDetails(skuDetailsParams)
details.skuDetailsList?.firstOrNull()?.let {
val flowParams = BillingFlowParams.newBuilder()

View file

@ -2,4 +2,5 @@ package dev.lucasnlm.external
class FeatureFlagManager : IFeatureFlagManager {
override fun isGameHistoryEnabled(): Boolean = false
override fun isRateUsEnabled(): Boolean = true
}

View file

@ -10,8 +10,8 @@ android {
defaultConfig {
// versionCode and versionName must be hardcoded to support F-droid
versionCode 805011
versionName '8.5.1'
versionCode 805021
versionName '8.5.2'
applicationId 'dev.lucasnlm.antimine'
minSdkVersion 23
targetSdkVersion 30

View file

@ -58,8 +58,15 @@ class WatchLevelFragment : CommonLevelFragment(R.layout.fragment_level) {
eventObserver.observe(
viewLifecycleOwner,
{
if (it == Event.StartNewGame) {
recyclerGrid.scrollToPosition(areaAdapter.itemCount / 2)
if (!gameViewModel.hasPlantedMines()) {
recyclerGrid.post {
levelSetup.value?.let { minefield ->
val size = dimensionRepository.areaSizeWithPadding()
val dx = minefield.width * size * 0.25f
val dy = minefield.height * size * 0.25f
recyclerGrid.smoothScrollBy(dx.toInt(), dy.toInt(), null, 200)
}
}
}
when (it) {