diff --git a/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/MonsterCompendiumStateHolder.kt b/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/MonsterCompendiumStateHolder.kt index c01ef51d..077b2674 100644 --- a/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/MonsterCompendiumStateHolder.kt +++ b/feature/monster-compendium/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/compendium/state/MonsterCompendiumStateHolder.kt @@ -84,6 +84,10 @@ class MonsterCompendiumStateHolder( } fun loadMonsters() = scope.launch { + fetchMonsterCompendium() + } + + private suspend fun fetchMonsterCompendium() { getMonsterCompendiumUseCase() .zip( getLastCompendiumScrollItemPositionUseCase() @@ -192,10 +196,7 @@ class MonsterCompendiumStateHolder( private fun navigateToCompendiumIndexFromMonsterIndex(monsterIndex: String) { flowOf(state.value.items) .map { items -> - items.indexOfFirst { - it is MonsterCompendiumItem.Item - && it.monster.index == monsterIndex - } + items.compendiumIndexOf(monsterIndex) }.onEach { compendiumIndex -> if (compendiumIndex < 0) throw NavigateToCompendiumIndexError(monsterIndex) } @@ -205,7 +206,10 @@ class MonsterCompendiumStateHolder( } .catch { error -> if (error is NavigateToCompendiumIndexError) { - loadMonsters() + fetchMonsterCompendium() + state.value.items.compendiumIndexOf(monsterIndex).takeIf { it >= 0 }?.let { + sendAction(GoToCompendiumIndex(it)) + } } else { analytics.logException(error) } @@ -213,6 +217,12 @@ class MonsterCompendiumStateHolder( .launchIn(scope) } + private fun List.compendiumIndexOf(monsterIndex: String): Int { + return indexOfFirst { + it is MonsterCompendiumItem.Item && it.monster.index == monsterIndex + } + } + private fun observeEvents() { scope.launch { folderPreviewResultListener.result.collect { event -> @@ -236,8 +246,11 @@ class MonsterCompendiumStateHolder( loadMonsters() }.launchIn(scope) - monsterRegistrationEventListener.collectOnSaved { - loadMonsters() + monsterRegistrationEventListener.collectOnSaved { monsterIndex -> + scope.launch { + fetchMonsterCompendium() + navigateToCompendiumIndexFromMonsterIndex(monsterIndex) + } }.launchIn(scope) } diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailFeature.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailFeature.kt index 63df509a..8e050727 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailFeature.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailFeature.kt @@ -31,10 +31,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import br.alexandregpereira.hunter.detail.ui.MonsterDeleteConfirmation import br.alexandregpereira.hunter.detail.ui.MonsterDetailOptionPicker import br.alexandregpereira.hunter.detail.ui.MonsterDetailScreen -import br.alexandregpereira.hunter.ui.compose.FormField import br.alexandregpereira.hunter.ui.compose.FormBottomSheet +import br.alexandregpereira.hunter.ui.compose.FormField import br.alexandregpereira.hunter.ui.compose.LoadingScreen import br.alexandregpereira.hunter.ui.compose.SwipeVerticalToDismiss import br.alexandregpereira.hunter.ui.theme.HunterTheme @@ -96,6 +97,13 @@ fun MonsterDetailFeature( onClosed = viewModel::onCloneFormClosed, onSaved = viewModel::onCloneFormSaved, ) + + MonsterDeleteConfirmation( + show = viewState.showDeleteConfirmation, + contentPadding = contentPadding, + onConfirmed = viewModel::onDeleteConfirmed, + onClosed = viewModel::onDeleteClosed + ) } } } diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailViewModel.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailViewModel.kt index bab49233..f217f84f 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailViewModel.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailViewModel.kt @@ -126,6 +126,10 @@ internal class MonsterDetailViewModel( fun onCloneFormSaved() = stateHolder.onCloneFormSaved() + fun onDeleteConfirmed() = stateHolder.onDeleteConfirmed() + + fun onDeleteClosed() = stateHolder.onDeleteClosed() + private fun MonsterDetailViewState.asMonsterDetailState(): MonsterDetailState { return MonsterDetailState( showDetail = showDetail, @@ -142,6 +146,7 @@ internal class MonsterDetailViewModel( isLoading = isLoading, showCloneForm = showCloneForm, monsterCloneName = monsterCloneName, + showDeleteConfirmation = showDeleteConfirmation, ) } } diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailViewState.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailViewState.kt index 72f59bf7..37b7571a 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailViewState.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/MonsterDetailViewState.kt @@ -28,6 +28,7 @@ data class MonsterDetailViewState( val isLoading: Boolean = true, val showCloneForm: Boolean = false, val monsterCloneName: String = "", + val showDeleteConfirmation: Boolean = false, ) fun SavedStateHandle.getState(): MonsterDetailViewState { diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDeleteConfirmation.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDeleteConfirmation.kt new file mode 100644 index 00000000..ebee26fc --- /dev/null +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDeleteConfirmation.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2022 Alexandre Gomes Pereira + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package br.alexandregpereira.hunter.detail.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import br.alexandregpereira.hunter.ui.compose.AppButton +import br.alexandregpereira.hunter.ui.compose.BottomSheet +import br.alexandregpereira.hunter.ui.compose.ScreenHeader + +@Composable +fun MonsterDeleteConfirmation( + show: Boolean, + contentPadding: PaddingValues = PaddingValues(), + onConfirmed: () -> Unit = {}, + onClosed: () -> Unit = {} +) = BottomSheet( + opened = show, + contentPadding = PaddingValues( + top = 16.dp + contentPadding.calculateTopPadding(), + bottom = 16.dp + contentPadding.calculateBottomPadding(), + start = 16.dp, + end = 16.dp, + ), + onClose = onClosed, +) { + Spacer(modifier = Modifier.height(16.dp)) + + ScreenHeader( + title = "Are you sure you want to delete this monster?", + ) + + Spacer(modifier = Modifier.height(32.dp)) + + AppButton( + text = "I'm sure", + onClick = onConfirmed + ) +} + +@Preview +@Composable +private fun MonsterDeleteConfirmationPreview() { + MonsterDeleteConfirmation( + show = true, + onConfirmed = {}, + onClosed = {} + ) +} \ No newline at end of file diff --git a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt index 47266719..46f5ca33 100644 --- a/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt +++ b/feature/monster-detail/android/src/main/kotlin/br/alexandregpereira/hunter/detail/ui/MonsterDetailScreen.kt @@ -49,7 +49,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -214,13 +214,13 @@ private fun OnMonsterChanged( pagerState: PagerState, onMonsterChanged: (monster: MonsterState) -> Unit ) { - var initialMonsterIndexState by remember { mutableStateOf(initialMonsterIndex) } + var initialMonsterIndexState by remember { mutableIntStateOf(initialMonsterIndex) } LaunchedEffect(key1 = initialMonsterIndex) { pagerState.scrollToPage(initialMonsterIndex) } - LaunchedEffect(pagerState) { + LaunchedEffect(pagerState, monsters) { snapshotFlow { pagerState.currentPage }.collect { page -> if (initialMonsterIndexState == initialMonsterIndex) { onMonsterChanged(monsters[page]) diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailAnalytics.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailAnalytics.kt index 0adc876b..3daa0bc2 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailAnalytics.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailAnalytics.kt @@ -115,4 +115,22 @@ class MonsterDetailAnalytics( ) ) } + + fun trackMonsterDetailDeleteConfirmed(monsterIndex: String) { + analytics.track( + eventName = "MonsterDetail - delete confirmed", + params = mapOf( + "monsterIndex" to monsterIndex, + ) + ) + } + + fun trackMonsterDetailDeleteCanceled(monsterIndex: String) { + analytics.track( + eventName = "MonsterDetail - delete canceled", + params = mapOf( + "monsterIndex" to monsterIndex, + ) + ) + } } \ No newline at end of file diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailState.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailState.kt index 9a27483f..7ae5a7c3 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailState.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailState.kt @@ -31,6 +31,7 @@ data class MonsterDetailState( val showDetail: Boolean = false, val showCloneForm: Boolean = false, val monsterCloneName: String = "", + val showDeleteConfirmation: Boolean = false, ) val MonsterDetailState.ShowOptions: MonsterDetailState diff --git a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStateHolder.kt b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStateHolder.kt index 5ac94a95..b22ab8e7 100644 --- a/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStateHolder.kt +++ b/feature/monster-detail/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/detail/MonsterDetailStateHolder.kt @@ -174,7 +174,8 @@ class MonsterDetailStateHolder( } DELETE -> { - deleteMonster() + analytics.trackMonsterDetailDeleteClicked(monsterIndex) + setState { copy(showDeleteConfirmation = true) } } CHANGE_TO_FEET -> { @@ -217,6 +218,17 @@ class MonsterDetailStateHolder( cloneMonster() } + fun onDeleteConfirmed() { + analytics.trackMonsterDetailDeleteConfirmed(monsterIndex) + setState { copy(showDeleteConfirmation = false) } + deleteMonster() + } + + fun onDeleteClosed() { + analytics.trackMonsterDetailDeleteCanceled(monsterIndex) + setState { copy(showDeleteConfirmation = false) } + } + private fun getMonsterDetail( monsterIndex: String = this.monsterIndex, invalidateCache: Boolean = false @@ -306,8 +318,8 @@ class MonsterDetailStateHolder( if (monsterIndexes.isEmpty()) { getMonsterDetail(monsterIndex, invalidateCache = true) .toMonsterDetailState() - .map { - it to monsterIndex + .map { state -> + state to monsterIndex } } else flowOf(currentState to currentMonsterIndex) }.flowOn(dispatcher) @@ -323,7 +335,6 @@ class MonsterDetailStateHolder( } private fun deleteMonster() { - analytics.trackMonsterDetailDeleteClicked(monsterIndex) deleteMonster(monsterIndex) .flowOn(dispatcher) .onEach { diff --git a/feature/monster-registration/event/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/event/Event.kt b/feature/monster-registration/event/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/event/Event.kt index 2a497fdd..786dbdef 100644 --- a/feature/monster-registration/event/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/event/Event.kt +++ b/feature/monster-registration/event/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/event/Event.kt @@ -14,7 +14,7 @@ sealed class MonsterRegistrationEvent { } sealed class MonsterRegistrationResult { - data object OnSaved : MonsterRegistrationResult() + data class OnSaved(val monsterIndex: String) : MonsterRegistrationResult() } interface MonsterRegistrationEventListener : EventListener @@ -22,8 +22,9 @@ interface MonsterRegistrationEventListener : EventListener fun EventListener.collectOnSaved( - onAction: () -> Unit -): Flow = events.map { it is MonsterRegistrationResult.OnSaved }.map { onAction() } + onAction: (String) -> Unit +): Flow = events.map { it as? MonsterRegistrationResult.OnSaved } + .map { event -> event?.let { onAction(it.monsterIndex) } } fun emptyMonsterRegistrationEventDispatcher(): MonsterRegistrationEventDispatcher { return object : MonsterRegistrationEventDispatcher { diff --git a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStateHolder.kt b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStateHolder.kt index e9eebd63..1127b6b8 100644 --- a/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStateHolder.kt +++ b/feature/monster-registration/state-holder/src/commonMain/kotlin/br/alexandregpereira/hunter/monster/registration/MonsterRegistrationStateHolder.kt @@ -53,7 +53,9 @@ class MonsterRegistrationStateHolder internal constructor( .flowOn(dispatcher) .onEach { onClose() - eventResultManager.dispatchEvent(MonsterRegistrationResult.OnSaved) + eventResultManager.dispatchEvent(MonsterRegistrationResult.OnSaved( + monsterIndex = state.value.monster.index + )) } .launchIn(scope) }