diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 3a2db40e4..999b86250 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -174,4 +174,6 @@ dependencies { androidTestImplementation(libs.bundles.android.test) testRuntimeOnly(libs.junit.vintage.engine) androidTestRuntimeOnly(libs.junit5.android.test.runner) + + implementation(libs.balloon) } diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/battle/BattleActivity.kt b/android/app/src/main/java/poke/rogue/helper/presentation/battle/BattleActivity.kt index 60657ca4f..bf8eea890 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/battle/BattleActivity.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/battle/BattleActivity.kt @@ -2,6 +2,8 @@ package poke.rogue.helper.presentation.battle import WeatherSpinnerAdapter import android.app.Activity +import android.content.Context +import android.content.Intent import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels @@ -17,13 +19,19 @@ import poke.rogue.helper.presentation.battle.view.itemSelectListener import poke.rogue.helper.presentation.util.context.colorOf import poke.rogue.helper.presentation.util.parcelable import poke.rogue.helper.presentation.util.repeatOnStarted +import poke.rogue.helper.presentation.util.serializable import poke.rogue.helper.presentation.util.view.setImage import timber.log.Timber class BattleActivity : ToolbarActivity(R.layout.activity_battle) { private val viewModel by viewModels { - BattleViewModel.factory(DefaultBattleRepository.instance(applicationContext)) + BattleViewModel.factory( + intent.getStringExtra(POKEMON_ID), + intent.serializable(SELECTION_TYPE), + DefaultBattleRepository.instance(applicationContext), + ) } + private val weatherAdapter by lazy { WeatherSpinnerAdapter(this) } @@ -128,4 +136,20 @@ class BattleActivity : ToolbarActivity(R.layout.activity_ } } } + + companion object { + private const val POKEMON_ID = "pokemonId" + private const val SELECTION_TYPE = "selectionType" + + fun intent( + context: Context, + pokemonId: String, + isMine: Boolean, + ): Intent = + Intent(context, BattleActivity::class.java).apply { + putExtra(POKEMON_ID, pokemonId) + val selectionType = if (isMine) SelectionType.MINE else SelectionType.OPPONENT + putExtra(SELECTION_TYPE, selectionType) + } + } } diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/battle/BattleViewModel.kt b/android/app/src/main/java/poke/rogue/helper/presentation/battle/BattleViewModel.kt index 29f478119..99e439214 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/battle/BattleViewModel.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/battle/BattleViewModel.kt @@ -32,7 +32,10 @@ import poke.rogue.helper.presentation.battle.model.toUi class BattleViewModel( private val battleRepository: BattleRepository, private val logger: AnalyticsLogger = analyticsLogger(), -) : ErrorHandleViewModel(logger), BattleNavigationHandler { + pokemonId: String? = null, + selectionType: SelectionType? = null, +) : ErrorHandleViewModel(logger), + BattleNavigationHandler { private val _weathers = MutableStateFlow(emptyList()) val weathers = _weathers.asStateFlow() @@ -48,7 +51,8 @@ class BattleViewModel( if (weathers.any { it.id == weather.id }.not()) return@combine null val selectedWeather = weathers.first { it.id == weather.id } // update selected weather - _selectedState.value = selectedState.value.copy(weather = BattleSelectionUiState.Selected(selectedWeather)) + _selectedState.value = + selectedState.value.copy(weather = BattleSelectionUiState.Selected(selectedWeather)) // return position weathers.indexOfFirst { it.id == weather.id } }.filterNotNull() @@ -72,12 +76,13 @@ class BattleViewModel( ) val isBattleFetchSuccessful = - battleResult.map { it.isSuccess() } + battleResult + .map { it.isSuccess() } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) init { initWeathers() - initSavedSelection() + handlePokemonSelection(pokemonId, selectionType) } private suspend fun fetchBattlePredictionResult(): BattlePredictionUiModel { @@ -105,21 +110,62 @@ class BattleViewModel( } } - private fun initSavedSelection() { + private fun handlePokemonSelection( + pokemonId: String?, + selectionType: SelectionType?, + ) { + when { + pokemonId == null -> { + fetchSavedMyPokemon() + fetchSavedOpponentPokemon() + } + + selectionType == SelectionType.MINE -> { + selectMyPokemon(pokemonId) + fetchSavedOpponentPokemon() + } + + selectionType == SelectionType.OPPONENT -> { + fetchSavedMyPokemon() + selectOpponentPokemon(pokemonId) + } + + else -> error("선택 타입 정보를 알 수 없습니다.") + } + } + + private fun fetchSavedMyPokemon() { viewModelScope.launch { - launch { - battleRepository.savedPokemonStream().first()?.let { - updateOpponentPokemon(it.toSelectionUi()) - } + battleRepository.savedPokemonWithSkillStream().first()?.let { (pokemon, skill) -> + updateMyPokemon(pokemon.toSelectionUi(), skill.toUi()) } - launch { - battleRepository.savedPokemonWithSkillStream().first()?.let { (pokemon, skill) -> - updateMyPokemon(pokemon.toSelectionUi(), skill.toUi()) - } + } + } + + private fun fetchSavedOpponentPokemon() { + viewModelScope.launch { + battleRepository.savedPokemonStream().first()?.let { + updateOpponentPokemon(it.toSelectionUi()) } } } + private fun selectMyPokemon(pokemonId: String) { + viewModelScope.launch { + val (pokemon, skill) = battleRepository.pokemonWithRandomSkill(pokemonId) + val selectionData = SelectionData.WithSkill(pokemon.toSelectionUi(), skill.toUi()) + updatePokemonSelection(selectionData) + } + } + + private fun selectOpponentPokemon(pokemonId: String) { + viewModelScope.launch { + val pokemon = battleRepository.pokemon(pokemonId) + val selectionData = SelectionData.WithoutSkill(pokemon.toSelectionUi()) + updatePokemonSelection(selectionData) + } + } + fun updateWeather(newWeather: WeatherUiModel) { viewModelScope.launch { val selectedWeather = BattleSelectionUiState.Selected(newWeather) @@ -191,19 +237,28 @@ class BattleViewModel( private fun previousSelection( hasSkillSelection: Boolean, previousPokemonSelection: PokemonSelectionUiModel, - ): SelectionData { - return if (hasSkillSelection) { + ): SelectionData = + if (hasSkillSelection) { val skill = requireNotNull(selectedState.value.skill.selectedData()) { "스킬이 선택되어야 합니다." } SelectionData.WithSkill(previousPokemonSelection, skill) } else { SelectionData.WithoutSkill(previousPokemonSelection) } - } companion object { - fun factory(battleRepository: BattleRepository): ViewModelProvider.Factory = - BaseViewModelFactory { BattleViewModel(battleRepository) } + fun factory( + pokemonId: String?, + selectionType: SelectionType?, + battleRepository: BattleRepository, + ): ViewModelProvider.Factory = + BaseViewModelFactory { + BattleViewModel( + battleRepository = battleRepository, + pokemonId = pokemonId, + selectionType = selectionType, + ) + } } } diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/battle/SelectionType.kt b/android/app/src/main/java/poke/rogue/helper/presentation/battle/SelectionType.kt new file mode 100644 index 000000000..0120b6175 --- /dev/null +++ b/android/app/src/main/java/poke/rogue/helper/presentation/battle/SelectionType.kt @@ -0,0 +1,6 @@ +package poke.rogue.helper.presentation.battle + +enum class SelectionType { + MINE, + OPPONENT, +} diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/biome/BiomeActivity.kt b/android/app/src/main/java/poke/rogue/helper/presentation/biome/BiomeActivity.kt index fff0d9aba..df9e1685e 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/biome/BiomeActivity.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/biome/BiomeActivity.kt @@ -22,7 +22,7 @@ import poke.rogue.helper.presentation.util.view.dp class BiomeActivity : ErrorHandleActivity(R.layout.activity_biome) { private val viewModel by viewModels { BiomeViewModel.factory( - DefaultBiomeRepository.instance(), + DefaultBiomeRepository.instance(applicationContext), ) } override val errorViewModel: ErrorHandleViewModel diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/biome/detail/BiomeDetailActivity.kt b/android/app/src/main/java/poke/rogue/helper/presentation/biome/detail/BiomeDetailActivity.kt index 96aafdd20..68d00c0b3 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/biome/detail/BiomeDetailActivity.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/biome/detail/BiomeDetailActivity.kt @@ -6,23 +6,28 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.widget.Toolbar import com.google.android.material.tabs.TabLayoutMediator +import com.skydoves.balloon.ArrowPositionRules +import com.skydoves.balloon.BalloonAnimation +import com.skydoves.balloon.BalloonSizeSpec +import com.skydoves.balloon.createBalloon import poke.rogue.helper.R import poke.rogue.helper.analytics.analyticsLogger import poke.rogue.helper.data.repository.DefaultBiomeRepository import poke.rogue.helper.databinding.ActivityBiomeDetailBinding import poke.rogue.helper.presentation.base.error.ErrorHandleActivity import poke.rogue.helper.presentation.base.error.ErrorHandleViewModel +import poke.rogue.helper.presentation.battle.BattleActivity import poke.rogue.helper.presentation.dex.detail.PokemonDetailActivity import poke.rogue.helper.presentation.util.context.startActivity +import poke.rogue.helper.presentation.util.context.stringOf import poke.rogue.helper.presentation.util.logClickEvent import poke.rogue.helper.presentation.util.repeatOnStarted -class BiomeDetailActivity : - ErrorHandleActivity(R.layout.activity_biome_detail) { +class BiomeDetailActivity : ErrorHandleActivity(R.layout.activity_biome_detail) { private lateinit var pagerAdapter: BiomeDetailPagerAdapter private val viewModel: BiomeDetailViewModel by viewModels { BiomeDetailViewModel.factory( - DefaultBiomeRepository.instance(), + DefaultBiomeRepository.instance(applicationContext), analyticsLogger(), ) } @@ -30,6 +35,23 @@ class BiomeDetailActivity : get() = viewModel override val toolbar: Toolbar get() = binding.toolbarBiomeDetail + private val tooltip by lazy { + createBalloon(this) { + setWidth(BalloonSizeSpec.WRAP) + setHeight(BalloonSizeSpec.WRAP) + setText(stringOf(R.string.biome_navigation_mode_info)) + setTextColorResource(R.color.poke_white) + setTextSize(11f) + setArrowPositionRules(ArrowPositionRules.ALIGN_ANCHOR) + setArrowSize(10) + setArrowPosition(0.0f) + setPadding(12) + setCornerRadius(8f) + setBackgroundColorResource(R.color.poke_red_20) + setBalloonAnimation(BalloonAnimation.ELASTIC) + build() + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,6 +63,7 @@ class BiomeDetailActivity : binding.lifecycleOwner = this initAdapter() initObservers() + initTooltip() } private fun initAdapter() { @@ -62,7 +85,7 @@ class BiomeDetailActivity : is BiomeDetailUiEvent.NavigateToNextBiomeDetail -> { val biomeId = event.biomeId startActivity { - putExtras(BiomeDetailActivity.intent(this@BiomeDetailActivity, biomeId)) + putExtras(intent(this@BiomeDetailActivity, biomeId)) analyticsLogger().logClickEvent(NAVIGATE_TO_NEXT_BIOME_DETAIL) } } @@ -72,11 +95,24 @@ class BiomeDetailActivity : putExtras(PokemonDetailActivity.intent(this@BiomeDetailActivity, pokemonId)) } } + + is BiomeDetailUiEvent.NavigateToBattle -> { + val pokemonId = event.pokemonId + startActivity { + putExtras(BattleActivity.intent(this@BiomeDetailActivity, pokemonId, isMine = false)) + } + } } } } } + private fun initTooltip() { + binding.tvNavigationMode.setOnClickListener { + tooltip.showAlignTop(it) + } + } + companion object { private const val BIOME_ID = "biomeId" private const val NAVIGATE_TO_NEXT_BIOME_DETAIL = "Nav_Next_Biome_Detail" diff --git a/android/app/src/main/java/poke/rogue/helper/presentation/biome/detail/BiomeDetailViewModel.kt b/android/app/src/main/java/poke/rogue/helper/presentation/biome/detail/BiomeDetailViewModel.kt index b75f4aae9..b8d16c55b 100644 --- a/android/app/src/main/java/poke/rogue/helper/presentation/biome/detail/BiomeDetailViewModel.kt +++ b/android/app/src/main/java/poke/rogue/helper/presentation/biome/detail/BiomeDetailViewModel.kt @@ -4,8 +4,10 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn @@ -23,9 +25,14 @@ import timber.log.Timber class BiomeDetailViewModel( private val biomeRepository: BiomeRepository, analytics: AnalyticsLogger, -) : ErrorHandleViewModel(analytics), BiomeDetailHandler, PokemonListNavigateHandler { +) : ErrorHandleViewModel(analytics), + BiomeDetailHandler, + PokemonListNavigateHandler { private val biomeId: MutableStateFlow = MutableStateFlow(IDLE_ID) + private val _isInBattleNavigationMode: MutableStateFlow = MutableStateFlow(false) + val isInBattleNavigationMode: StateFlow = _isInBattleNavigationMode.asStateFlow() + // TODO : 아직 작업 다 안끝났음 val uiState: StateFlow = combine( @@ -58,20 +65,42 @@ class BiomeDetailViewModel( true, ) + init { + viewModelScope.launch { + biomeRepository + .isBattleNavigationModeStream() + .firstOrNull() + ?.let { _isInBattleNavigationMode.value = it } + } + } + fun init(id: String) { if (id.isBlank()) return handlePokemonError(IllegalArgumentException("biomeId is blank")) biomeId.value = id } + fun changeNavigationMode(isBattleNavigationMode: Boolean) { + _isInBattleNavigationMode.value = isBattleNavigationMode + viewModelScope.launch { + biomeRepository.saveNavigationMode(isBattleNavigationMode) + } + } + override fun navigateToBiomeDetail(id: String) { viewModelScope.launch { _uiEvent.emit(BiomeDetailUiEvent.NavigateToNextBiomeDetail(id)) } } - override fun navigateToPokemonDetail(id: String) { + override fun navigateToPokemonDetail(pokemonId: String) { + val uiEvent = + if (isInBattleNavigationMode.value) { + BiomeDetailUiEvent.NavigateToBattle(pokemonId) + } else { + BiomeDetailUiEvent.NavigateToPokemonDetail(pokemonId) + } viewModelScope.launch { - _uiEvent.emit(BiomeDetailUiEvent.NavigateToPokemonDetail(id)) + _uiEvent.emit(uiEvent) } } @@ -91,6 +120,8 @@ sealed interface BiomeDetailUiEvent { data class NavigateToNextBiomeDetail(val biomeId: String) : BiomeDetailUiEvent data class NavigateToPokemonDetail(val pokemonId: String) : BiomeDetailUiEvent + + data class NavigateToBattle(val pokemonId: String) : BiomeDetailUiEvent } interface BiomeDetailHandler { diff --git a/android/app/src/main/res/drawable/icon_info.xml b/android/app/src/main/res/drawable/icon_info.xml new file mode 100644 index 000000000..2c1bea68a --- /dev/null +++ b/android/app/src/main/res/drawable/icon_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/selector_toggle_btn.xml b/android/app/src/main/res/drawable/selector_toggle_btn.xml new file mode 100644 index 000000000..c3a2423bc --- /dev/null +++ b/android/app/src/main/res/drawable/selector_toggle_btn.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/shape_toggle_thumb.xml b/android/app/src/main/res/drawable/shape_toggle_thumb.xml new file mode 100644 index 000000000..95a30a63f --- /dev/null +++ b/android/app/src/main/res/drawable/shape_toggle_thumb.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/android/app/src/main/res/layout/activity_biome_detail.xml b/android/app/src/main/res/layout/activity_biome_detail.xml index edb9c566a..2f1dd0397 100644 --- a/android/app/src/main/res/layout/activity_biome_detail.xml +++ b/android/app/src/main/res/layout/activity_biome_detail.xml @@ -26,7 +26,6 @@ app:title="@{vm.uiState.name}" /> + + + + + app:layout_constraintGuide_percent="0.4" /> diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 518d845bf..ebaa60a13 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -98,6 +98,8 @@ 야생 다음 바이옴 + 배틀 모드 설정 + "포켓몬 클릭 시 기본적으로 상세 페이지로 이동합니다.\n 배틀 페이지로 가려면 배틀 모드를 설정해주세요!" 등장하는 체육관 관장이 없어요 diff --git a/android/data/src/main/java/poke/rogue/helper/data/datasource/LocalNavigationDataSource.kt b/android/data/src/main/java/poke/rogue/helper/data/datasource/LocalNavigationDataSource.kt new file mode 100644 index 000000000..739d02e39 --- /dev/null +++ b/android/data/src/main/java/poke/rogue/helper/data/datasource/LocalNavigationDataSource.kt @@ -0,0 +1,26 @@ +package poke.rogue.helper.data.datasource + +import android.content.Context +import kotlinx.coroutines.flow.Flow +import poke.rogue.helper.local.datastore.NavigationModeDataStore + +class LocalNavigationDataSource( + private val dataStore: NavigationModeDataStore, +) { + suspend fun saveNavigationMode(isBattleNavigationMode: Boolean) { + dataStore.saveNavigationMode(isBattleNavigationMode) + } + + fun isBattleNavigationModeStream(): Flow = dataStore.isBattleNavigationMode() + + companion object { + private var instance: LocalNavigationDataSource? = null + + fun instance(context: Context): LocalNavigationDataSource = + instance ?: LocalNavigationDataSource( + NavigationModeDataStore(context), + ).also { + instance = it + } + } +} diff --git a/android/data/src/main/java/poke/rogue/helper/data/di/DataSourceModule.kt b/android/data/src/main/java/poke/rogue/helper/data/di/DataSourceModule.kt index 1b88b2a0c..b32ce7c5e 100644 --- a/android/data/src/main/java/poke/rogue/helper/data/di/DataSourceModule.kt +++ b/android/data/src/main/java/poke/rogue/helper/data/di/DataSourceModule.kt @@ -4,6 +4,7 @@ import org.koin.core.module.dsl.singleOf import org.koin.dsl.module import poke.rogue.helper.data.datasource.LocalBattleDataSource import poke.rogue.helper.data.datasource.LocalDexDataSource +import poke.rogue.helper.data.datasource.LocalNavigationDataSource import poke.rogue.helper.data.datasource.LocalTypeDataSource import poke.rogue.helper.data.datasource.RemoteAbilityDataSource import poke.rogue.helper.data.datasource.RemoteBattleDataSource @@ -16,6 +17,7 @@ internal val dataSourceModule singleOf(::LocalBattleDataSource) singleOf(::LocalDexDataSource) singleOf(::LocalTypeDataSource) + singleOf(::LocalNavigationDataSource) singleOf(::RemoteBattleDataSource) singleOf(::RemoteAbilityDataSource) diff --git a/android/data/src/main/java/poke/rogue/helper/data/repository/BattleRepository.kt b/android/data/src/main/java/poke/rogue/helper/data/repository/BattleRepository.kt index 5dff8a364..a527dc05a 100644 --- a/android/data/src/main/java/poke/rogue/helper/data/repository/BattleRepository.kt +++ b/android/data/src/main/java/poke/rogue/helper/data/repository/BattleRepository.kt @@ -33,4 +33,8 @@ interface BattleRepository { fun savedPokemonStream(): Flow fun savedPokemonWithSkillStream(): Flow + + suspend fun pokemon(pokemonId: String): Pokemon + + suspend fun pokemonWithRandomSkill(pokemonId: String): PokemonWithSkill } diff --git a/android/data/src/main/java/poke/rogue/helper/data/repository/BiomeRepository.kt b/android/data/src/main/java/poke/rogue/helper/data/repository/BiomeRepository.kt index c85b1314d..ea64c01ee 100644 --- a/android/data/src/main/java/poke/rogue/helper/data/repository/BiomeRepository.kt +++ b/android/data/src/main/java/poke/rogue/helper/data/repository/BiomeRepository.kt @@ -1,5 +1,6 @@ package poke.rogue.helper.data.repository +import kotlinx.coroutines.flow.Flow import poke.rogue.helper.data.model.Biome import poke.rogue.helper.data.model.BiomeDetail @@ -9,4 +10,8 @@ interface BiomeRepository { suspend fun biomes(query: String): List suspend fun biomeDetail(id: String): BiomeDetail + + suspend fun saveNavigationMode(isBattleNavigationMode: Boolean) + + fun isBattleNavigationModeStream(): Flow } diff --git a/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultBattleRepository.kt b/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultBattleRepository.kt index 5cfb30b30..b94a026cf 100644 --- a/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultBattleRepository.kt +++ b/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultBattleRepository.kt @@ -79,17 +79,24 @@ class DefaultBattleRepository( it.id == skillId } ?: error("아이디에 해당하는 스킬이 존재하지 않습니다. id: $skillId") + override suspend fun pokemon(pokemonId: String): Pokemon = pokemonRepository.pokemon(pokemonId) + + override suspend fun pokemonWithRandomSkill(pokemonId: String): PokemonWithSkill { + val pokemon = pokemonRepository.pokemon(pokemonId) + val skill = availableSkills(pokemon.dexNumber).firstOrNull() ?: error("배정 가능한 스킬이 존재 하지 않습니다. - dexNumber : ${pokemon.dexNumber}") + return PokemonWithSkill(pokemon, skill) + } + companion object { private var instance: BattleRepository? = null - fun instance(context: Context): BattleRepository { - return instance ?: DefaultBattleRepository( + fun instance(context: Context): BattleRepository = + instance ?: DefaultBattleRepository( LocalBattleDataSource.instance(context), RemoteBattleDataSource.instance(), DefaultDexRepository.instance(), ).also { instance = it } - } } } diff --git a/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultBiomeRepository.kt b/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultBiomeRepository.kt index f5f8fe3f1..7e2e6d3dc 100644 --- a/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultBiomeRepository.kt +++ b/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultBiomeRepository.kt @@ -1,7 +1,10 @@ package poke.rogue.helper.data.repository +import android.content.Context +import kotlinx.coroutines.flow.Flow import poke.rogue.helper.analytics.AnalyticsLogger import poke.rogue.helper.analytics.analyticsLogger +import poke.rogue.helper.data.datasource.LocalNavigationDataSource import poke.rogue.helper.data.datasource.RemoteBiomeDataSource import poke.rogue.helper.data.model.Biome import poke.rogue.helper.data.model.BiomeDetail @@ -11,6 +14,7 @@ import poke.rogue.helper.stringmatcher.has class DefaultBiomeRepository( private val remoteBiomeDataSource: RemoteBiomeDataSource, private val analyticsLogger: AnalyticsLogger, + private val localNavigationDataSource: LocalNavigationDataSource, ) : BiomeRepository { private var cachedBiomes: List = emptyList() @@ -28,23 +32,28 @@ class DefaultBiomeRepository( return biomes().filter { it.name.has(query) } } - override suspend fun biomeDetail(id: String): BiomeDetail { - return remoteBiomeDataSource.biomeDetail(id).also { + override suspend fun biomeDetail(id: String): BiomeDetail = + remoteBiomeDataSource.biomeDetail(id).also { analyticsLogger.logBiomeDetail(id, it.name) } + + override suspend fun saveNavigationMode(isBattleNavigationMode: Boolean) { + localNavigationDataSource.saveNavigationMode(isBattleNavigationMode) } + override fun isBattleNavigationModeStream(): Flow = localNavigationDataSource.isBattleNavigationModeStream() + companion object { private var instance: DefaultBiomeRepository? = null - fun instance(): DefaultBiomeRepository { - return instance + fun instance(context: Context): DefaultBiomeRepository = + instance ?: DefaultBiomeRepository( RemoteBiomeDataSource.instance(), analyticsLogger(), + LocalNavigationDataSource.instance(context), ).also { instance = it } - } } } diff --git a/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultDexRepository.kt b/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultDexRepository.kt index 818515b2f..33b31fb55 100644 --- a/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultDexRepository.kt +++ b/android/data/src/main/java/poke/rogue/helper/data/repository/DefaultDexRepository.kt @@ -57,13 +57,12 @@ class DefaultDexRepository( name: String, sort: PokemonSort, filters: List, - ): List { - return if (name.isBlank()) { + ): List = + if (name.isBlank()) { pokemons() } else { pokemons().filter { it.name.has(name) } }.toFilteredPokemons(sort, filters) - } override suspend fun pokemonDetail(id: String): PokemonDetail { val allBiomes = biomeRepository.biomes() @@ -101,8 +100,8 @@ class DefaultDexRepository( private fun List.toFilteredPokemons( sort: PokemonSort, pokemonFilters: List, - ): List { - return this + ): List = + this .filter { pokemon -> pokemonFilters.all { pokemonFilter -> when (pokemonFilter) { @@ -110,9 +109,7 @@ class DefaultDexRepository( is PokemonFilter.ByGeneration -> pokemon.generation == pokemonFilter.generation } } - } - .sortedWith(sort) - } + }.sortedWith(sort) companion object { private var instance: DexRepository? = null @@ -125,16 +122,15 @@ class DefaultDexRepository( RemoteDexDataSource.instance(), LocalDexDataSource.instance(context), GlideImageCacher.instance(), - DefaultBiomeRepository.instance(), + DefaultBiomeRepository.instance(context), analyticsLogger(), ) } - fun instance(): DexRepository { - return requireNotNull(instance) { + fun instance(): DexRepository = + requireNotNull(instance) { "DexRepository is not initialized" } - } } } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 708719ddd..2c17db2e0 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] applicationId = "poke.rogue.helper" +balloon = "1.6.8" compileSdk = "34" koin = "4.0.0" minSdk = "26" @@ -63,6 +64,7 @@ firebaseCrashlyticsBuildtools = "3.0.2" agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } # kotlin +balloon = { module = "com.github.skydoves:balloon", version.ref = "balloon" } kotlin = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } kotlin-gradleplugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } diff --git a/android/local/src/main/java/poke/rogue/helper/local/datastore/NavigationModeDataStore.kt b/android/local/src/main/java/poke/rogue/helper/local/datastore/NavigationModeDataStore.kt new file mode 100644 index 000000000..68aee844c --- /dev/null +++ b/android/local/src/main/java/poke/rogue/helper/local/datastore/NavigationModeDataStore.kt @@ -0,0 +1,27 @@ +package poke.rogue.helper.local.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.mapNotNull + +class NavigationModeDataStore(private val context: Context) { + private val Context.datastore: DataStore by preferencesDataStore(name = NAVIGATION_MODE_PREFERENCE_NAME) + + suspend fun saveNavigationMode(isBattleNavigationMode: Boolean) { + context.datastore.edit { + it[IS_BATTLE_NAVIGATION_MODE_KEY] = isBattleNavigationMode + } + } + + fun isBattleNavigationMode(): Flow = context.datastore.data.mapNotNull { it[IS_BATTLE_NAVIGATION_MODE_KEY] } + + private companion object { + const val NAVIGATION_MODE_PREFERENCE_NAME = "navigationMode" + val IS_BATTLE_NAVIGATION_MODE_KEY = booleanPreferencesKey("is_battle_navigation_mode") + } +} diff --git a/android/local/src/main/java/poke/rogue/helper/local/di/DataStoreModule.kt b/android/local/src/main/java/poke/rogue/helper/local/di/DataStoreModule.kt index cb741b8d1..8e1ca5089 100644 --- a/android/local/src/main/java/poke/rogue/helper/local/di/DataStoreModule.kt +++ b/android/local/src/main/java/poke/rogue/helper/local/di/DataStoreModule.kt @@ -3,9 +3,11 @@ package poke.rogue.helper.local.di import org.koin.core.module.dsl.singleOf import org.koin.dsl.module import poke.rogue.helper.local.datastore.BattleDataStore +import poke.rogue.helper.local.datastore.NavigationModeDataStore internal val dataStoreModule get() = module { singleOf(::BattleDataStore) + singleOf(::NavigationModeDataStore) } diff --git a/android/testing/src/main/java/poke/rogue/helper/testing/data/repository/FakeBiomeRepository.kt b/android/testing/src/main/java/poke/rogue/helper/testing/data/repository/FakeBiomeRepository.kt index 975d40c91..66a4c147c 100644 --- a/android/testing/src/main/java/poke/rogue/helper/testing/data/repository/FakeBiomeRepository.kt +++ b/android/testing/src/main/java/poke/rogue/helper/testing/data/repository/FakeBiomeRepository.kt @@ -1,5 +1,6 @@ package poke.rogue.helper.testing.data.repository +import kotlinx.coroutines.flow.Flow import poke.rogue.helper.data.model.Biome import poke.rogue.helper.data.model.BiomeDetail import poke.rogue.helper.data.model.NextBiome @@ -14,12 +15,18 @@ import poke.rogue.helper.stringmatcher.has class FakeBiomeRepository : BiomeRepository { override suspend fun biomes(): List = BIOMES - override suspend fun biomes(query: String): List { - return BIOMES.filter { biome -> biome.name.has(query) } - } + override suspend fun biomes(query: String): List = BIOMES.filter { biome -> biome.name.has(query) } override suspend fun biomeDetail(id: String): BiomeDetail = BIOME_DETAIL[id] ?: throw IllegalArgumentException("Invalid biome ID") + override suspend fun saveNavigationMode(isBattleNavigationMode: Boolean) { + TODO("Not yet implemented") + } + + override fun isBattleNavigationModeStream(): Flow { + TODO("Not yet implemented") + } + companion object { val BIOMES: List = listOf(