diff --git a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RoadsRootComponent.kt b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RoadsRootComponent.kt index 409c79894..9502fa67a 100644 --- a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RoadsRootComponent.kt +++ b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RoadsRootComponent.kt @@ -8,6 +8,7 @@ import com.arkivanov.essenty.backhandler.BackHandlerOwner import com.egoriku.grodnoroads.eventreporting.domain.model.ReportParams import com.egoriku.grodnoroads.extensions.common.StateData import com.egoriku.grodnoroads.screen.main.MainComponent +import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory.MigrationModel import com.egoriku.grodnoroads.screen.root.store.headlamp.HeadLampType import com.egoriku.grodnoroads.setting.alerts.domain.component.AlertsComponent import com.egoriku.grodnoroads.setting.appearance.domain.component.AppearanceComponent @@ -22,6 +23,7 @@ interface RoadsRootComponent : BackHandlerOwner { val childStack: Value> val childSlot: Value> + val migrationModel: Flow fun closeReporting() fun processReporting(params: ReportParams) diff --git a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RoadsRootComponentImpl.kt b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RoadsRootComponentImpl.kt index b65171260..f77667d28 100644 --- a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RoadsRootComponentImpl.kt +++ b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RoadsRootComponentImpl.kt @@ -13,6 +13,7 @@ import com.egoriku.grodnoroads.screen.main.buildMainComponent import com.egoriku.grodnoroads.screen.root.RoadsRootComponent.Child import com.egoriku.grodnoroads.screen.root.store.RootStore import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory.Intent +import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory.MigrationModel import com.egoriku.grodnoroads.screen.root.store.headlamp.HeadLampType import com.egoriku.grodnoroads.setting.alerts.domain.component.buildAlertsComponent import com.egoriku.grodnoroads.setting.appearance.domain.component.buildAppearanceComponent @@ -63,6 +64,8 @@ class RoadsRootComponentImpl( } } + override val migrationModel: Flow = rootStore.states.map { it.migrationModel } + override val headlampDialogState: Flow = rootStore.states.map { it.headLampType } override fun closeHeadlampDialog() { diff --git a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RootContent.kt b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RootContent.kt index e57f2d73d..8b32bbab1 100644 --- a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RootContent.kt +++ b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/RootContent.kt @@ -1,12 +1,26 @@ package com.egoriku.grodnoroads.screen.root -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import android.content.ActivityNotFoundException +import android.content.Intent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material3.Icon import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.FaultyDecomposeApi import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children @@ -14,9 +28,14 @@ import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.* import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.predictiveback.predictiveBackAnimation import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState import com.arkivanov.decompose.router.slot.ChildSlot +import com.egoriku.grodnoroads.R import com.egoriku.grodnoroads.eventreporting.EventReportingScreen +import com.egoriku.grodnoroads.foundation.uikit.button.PrimaryButton +import com.egoriku.grodnoroads.resources.R.drawable +import com.egoriku.grodnoroads.resources.R.string import com.egoriku.grodnoroads.screen.main.MainUi import com.egoriku.grodnoroads.screen.root.RoadsRootComponent.Child +import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory.MigrationModel import com.egoriku.grodnoroads.screen.root.store.headlamp.HeadLampType import com.egoriku.grodnoroads.screen.root.ui.HeadLampDialog import com.egoriku.grodnoroads.setting.alerts.AlertsScreen @@ -24,6 +43,7 @@ import com.egoriku.grodnoroads.setting.appearance.screen.AppearanceScreen import com.egoriku.grodnoroads.setting.changelog.screen.ChangelogScreen import com.egoriku.grodnoroads.setting.faq.screen.FaqScreen import com.egoriku.grodnoroads.setting.map.MapSettingsScreen +import com.egoriku.grodnoroads.setting.screen.ui.foundation.SocialNetwork @OptIn(FaultyDecomposeApi::class, ExperimentalDecomposeApi::class) @Composable @@ -31,66 +51,180 @@ fun RootContent(rootComponent: RoadsRootComponent) { Surface(modifier = Modifier.fillMaxSize()) { val dialogState by rootComponent.headlampDialogState.collectAsState(initial = HeadLampType.None) - if (dialogState != HeadLampType.None) { - HeadLampDialog( - headlampType = dialogState, - onClose = rootComponent::closeHeadlampDialog - ) - } - Box { - Children( - stack = rootComponent.childStack, - animation = predictiveBackAnimation( - backHandler = rootComponent.backHandler, - animation = stackAnimation { _, _, direction -> - if (direction.isFront) { - slide() + fade() - } else { - scale(frontFactor = 1F, backFactor = 0.7F) + fade() - } - }, - onBack = rootComponent::onBack, - ), - - ) { - when (val child = it.instance) { - is Child.Main -> MainUi(component = child.component) - is Child.Appearance -> AppearanceScreen( - appearanceComponent = child.appearanceComponent, - onBack = rootComponent::onBack - ) + val migrationModel by rootComponent.migrationModel.collectAsState(MigrationModel()) - is Child.Map -> MapSettingsScreen( - mapSettingsComponent = child.mapSettingsComponent, - onBack = rootComponent::onBack - ) + if (migrationModel.enabled) { + MigrationScreen(migrationModel) + } else { + if (dialogState != HeadLampType.None) { + HeadLampDialog( + headlampType = dialogState, + onClose = rootComponent::closeHeadlampDialog + ) + } + Box { + Children( + stack = rootComponent.childStack, + animation = predictiveBackAnimation( + backHandler = rootComponent.backHandler, + animation = stackAnimation { _, _, direction -> + if (direction.isFront) { + slide() + fade() + } else { + scale(frontFactor = 1F, backFactor = 0.7F) + fade() + } + }, + onBack = rootComponent::onBack, + ), - is Child.Alerts -> AlertsScreen( - alertsComponent = child.alertsComponent, - onBack = rootComponent::onBack - ) + ) { + when (val child = it.instance) { + is Child.Main -> MainUi(component = child.component) + is Child.Appearance -> AppearanceScreen( + appearanceComponent = child.appearanceComponent, + onBack = rootComponent::onBack + ) - is Child.Changelog -> ChangelogScreen( - changelogComponent = child.changelogComponent, - onBack = rootComponent::onBack, - ) + is Child.Map -> MapSettingsScreen( + mapSettingsComponent = child.mapSettingsComponent, + onBack = rootComponent::onBack + ) + + is Child.Alerts -> AlertsScreen( + alertsComponent = child.alertsComponent, + onBack = rootComponent::onBack + ) - is Child.NextFeatures -> TODO() - is Child.BetaFeatures -> TODO() - is Child.FAQ -> FaqScreen( - faqComponent = child.faqComponent, - onBack = rootComponent::onBack + is Child.Changelog -> ChangelogScreen( + changelogComponent = child.changelogComponent, + onBack = rootComponent::onBack, + ) + + is Child.NextFeatures -> TODO() + is Child.BetaFeatures -> TODO() + is Child.FAQ -> FaqScreen( + faqComponent = child.faqComponent, + onBack = rootComponent::onBack + ) + } + } + + val childSlot by rootComponent.childSlot.subscribeAsState() + childSlot.onChild { + EventReportingScreen( + onClose = rootComponent::closeReporting, + onReport = rootComponent::processReporting ) } } + } + } +} + +@Composable +fun MigrationScreen(migrationModel: MigrationModel) { + val context = LocalContext.current + + val onOpenBrowser = { url: String -> + val intent = Intent(Intent.ACTION_VIEW).apply { + data = url.toUri() + } + context.startActivity(intent) + } + + val openPlayStore = { newPackage: String -> + try { + context.startActivity( + Intent( + Intent.ACTION_VIEW, + "market://details?id=$newPackage".toUri() + ) + ) + } catch (exception: ActivityNotFoundException) { + context.startActivity( + Intent( + Intent.ACTION_VIEW, + "https://play.google.com/store/apps/details?id=$newPackage".toUri() + ) + ) + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(80.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_old), + contentDescription = null + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null + ) + Image( + painter = painterResource(id = R.drawable.ic_new), + contentDescription = null + ) + } + Text( + modifier = Modifier.fillMaxWidth(), + text = """ + Уважаемые пользователи! + + К сожалению, из-за санкций и нововведений Google, приложение было безвозвратно удалено. + + Текущее приложение больше не поддерживается, а новое уже доступно для скачивания ниже. + + Присоединяйтесь к каналу в Telegram, чтобы быть в курсе всех новостей и обновлений. + + С уважением, + команда За Рулем Гродно + """.trimIndent() + ) - val childSlot by rootComponent.childSlot.subscribeAsState() - childSlot.onChild { - EventReportingScreen( - onClose = rootComponent::closeReporting, - onReport = rootComponent::processReporting + val channelUrl = stringResource(string.tg_channel_link) + + SocialNetwork( + modifier = Modifier.align(Alignment.CenterHorizontally), + title = "Канал в Telegram", + onClick = { onOpenBrowser(channelUrl) } + ) { + Icon( + painter = painterResource(drawable.ic_telegram), + contentDescription = stringResource(string.social_telegram_channel) ) } + + } + PrimaryButton( + onClick = { + if (migrationModel.newPackage.isNotEmpty()) { + openPlayStore(migrationModel.newPackage) + } else { + onOpenBrowser(migrationModel.link) + } + } + ) { + Text("Скачать новое приложение") } } } diff --git a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/koin/RootModule.kt b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/koin/RootModule.kt index 8d81bbe40..8709dd968 100644 --- a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/koin/RootModule.kt +++ b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/koin/RootModule.kt @@ -1,13 +1,18 @@ package com.egoriku.grodnoroads.screen.root.koin +import com.egoriku.grodnoroads.screen.root.migration.MigrationRepository import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory +import org.koin.core.module.dsl.factoryOf import org.koin.dsl.module val rootModule = module { factory { RootStoreFactory( storeFactory = get(), - dataStore = get() + dataStore = get(), + migrationRepository = get() ).create() } + + factoryOf(::MigrationRepository) } \ No newline at end of file diff --git a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/migration/MigrationDTO.kt b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/migration/MigrationDTO.kt new file mode 100644 index 000000000..c11518d42 --- /dev/null +++ b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/migration/MigrationDTO.kt @@ -0,0 +1,20 @@ +package com.egoriku.grodnoroads.screen.root.migration + +import androidx.annotation.Keep +import com.google.firebase.database.PropertyName + +@Keep +class MigrationDTO( + + @PropertyName("enabled") + @JvmField + val enabled: Boolean = false, + + @PropertyName("link") + @JvmField + val link: String = "", + + @PropertyName("newPackage") + @JvmField + val newPackage: String = "" +) \ No newline at end of file diff --git a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/migration/MigrationRepository.kt b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/migration/MigrationRepository.kt new file mode 100644 index 000000000..822a4141d --- /dev/null +++ b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/migration/MigrationRepository.kt @@ -0,0 +1,36 @@ +package com.egoriku.grodnoroads.screen.root.migration + +import com.egoriku.grodnoroads.extensions.awaitValueEventListener +import com.egoriku.grodnoroads.extensions.common.ResultOf +import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory.MigrationModel +import com.google.firebase.database.DatabaseReference +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class MigrationRepository(private val databaseReference: DatabaseReference) { + + fun loadAsFlow(): Flow> = databaseReference + .child("migration") + .awaitValueEventListener() + .map { resultOf -> + when (resultOf) { + is ResultOf.Failure -> { + ResultOf.Failure(resultOf.exception) + } + is ResultOf.Success -> { + val dto = resultOf.value.firstOrNull() ?: MigrationDTO() + + ResultOf.Success( + MigrationModel( + enabled = dto.enabled, + link = dto.link, + newPackage = dto.newPackage + ) + ) + } + } + } + .flowOn(Dispatchers.IO) +} \ No newline at end of file diff --git a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/store/RootStoreFactory.kt b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/store/RootStoreFactory.kt index 020f6f608..ecc3599fd 100644 --- a/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/store/RootStoreFactory.kt +++ b/app/android/src/main/kotlin/com/egoriku/grodnoroads/screen/root/store/RootStoreFactory.kt @@ -7,6 +7,8 @@ import com.arkivanov.mvikotlin.core.store.Store import com.arkivanov.mvikotlin.core.store.StoreFactory import com.arkivanov.mvikotlin.core.utils.ExperimentalMviKotlinApi import com.arkivanov.mvikotlin.extensions.coroutines.coroutineExecutorFactory +import com.egoriku.grodnoroads.extensions.common.ResultOf +import com.egoriku.grodnoroads.screen.root.migration.MigrationRepository import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory.Intent import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory.Intent.CloseDialog import com.egoriku.grodnoroads.screen.root.store.RootStoreFactory.Message.UpdateHeadLampDialog @@ -26,7 +28,8 @@ interface RootStore : Store class RootStoreFactory( private val storeFactory: StoreFactory, - private val dataStore: DataStore + private val dataStore: DataStore, + private val migrationRepository: MigrationRepository ) { sealed interface Intent { @@ -34,13 +37,21 @@ class RootStoreFactory( } sealed interface Message { - data class NewState(val state: State) : Message + data class OnTheme(val theme: Theme) : Message + data class OnMigration(val migrationModel: MigrationModel) : Message data class UpdateHeadLampDialog(val headLampType: HeadLampType) : Message } data class State( val theme: Theme? = null, - val headLampType: HeadLampType = HeadLampType.None + val headLampType: HeadLampType = HeadLampType.None, + val migrationModel: MigrationModel = MigrationModel() + ) + + data class MigrationModel( + val enabled: Boolean = false, + val link: String = "", + val newPackage: String = "" ) @OptIn(ExperimentalMviKotlinApi::class) @@ -51,15 +62,28 @@ class RootStoreFactory( onAction { dataStore.data .map { preferences -> - State(theme = Theme.fromOrdinal(preferences.appTheme.theme)) + Theme.fromOrdinal(preferences.appTheme.theme) } .distinctUntilChanged() - .onEach { dispatch(Message.NewState(state = it)) } + .onEach { dispatch(Message.OnTheme(theme = it)) } .launchIn(this) launch { dispatch(UpdateHeadLampDialog(headLampType = HeadLampDispatcher.calculateType())) } + + launch { + migrationRepository.loadAsFlow() + .collect { + when (it) { + is ResultOf.Failure -> {} + is ResultOf.Success -> { + dispatch(Message.OnMigration(it.value)) + } + } + } + + } } onIntent { launch { @@ -70,8 +94,9 @@ class RootStoreFactory( bootstrapper = SimpleBootstrapper(Unit), reducer = { message: Message -> when (message) { - is Message.NewState -> copy(theme = message.state.theme) + is Message.OnTheme -> copy(theme = message.theme) is UpdateHeadLampDialog -> copy(headLampType = message.headLampType) + is Message.OnMigration -> copy(migrationModel = message.migrationModel) } } ) {} diff --git a/app/android/src/main/res/drawable/ic_new.png b/app/android/src/main/res/drawable/ic_new.png new file mode 100644 index 000000000..ea1dc1872 Binary files /dev/null and b/app/android/src/main/res/drawable/ic_new.png differ diff --git a/app/android/src/main/res/drawable/ic_old.png b/app/android/src/main/res/drawable/ic_old.png new file mode 100644 index 000000000..f1821623e Binary files /dev/null and b/app/android/src/main/res/drawable/ic_old.png differ diff --git a/app/android/version.properties b/app/android/version.properties index c5c5c1ac6..16c8f0493 100644 --- a/app/android/version.properties +++ b/app/android/version.properties @@ -1,3 +1,3 @@ -BUILD_VERSION=9 +BUILD_VERSION=10 SUB_VERSION=2 VERSION=1 diff --git a/features/settings/src/main/kotlin/com/egoriku/grodnoroads/setting/screen/ui/foundation/SocialNetwork.kt b/features/settings/src/main/kotlin/com/egoriku/grodnoroads/setting/screen/ui/foundation/SocialNetwork.kt index 2bba961f6..d98332b7e 100644 --- a/features/settings/src/main/kotlin/com/egoriku/grodnoroads/setting/screen/ui/foundation/SocialNetwork.kt +++ b/features/settings/src/main/kotlin/com/egoriku/grodnoroads/setting/screen/ui/foundation/SocialNetwork.kt @@ -16,11 +16,12 @@ import com.egoriku.grodnoroads.resources.R @Composable fun SocialNetwork( + modifier: Modifier = Modifier, title: String, onClick: () -> Unit, content: @Composable RowScope.() -> Unit ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { SecondaryCircleButton(onClick = onClick, content = content) Text( modifier = Modifier.padding(top = 8.dp),