From 4c6735746e5323bb50b24916e6e7c70b7013c5ed Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 15 Sep 2026 10:59:24 +0300 Subject: [PATCH] add routing screen --- .../gigawear/data/HealthCheckImpl.kt | 17 ++++++ .../gigawear/data/LogoutActionImpl.kt | 10 ++++ .../gigawear/domain/HealthCheck.kt | 5 ++ .../presentation/GigaWearRootComponent.kt | 2 +- .../gigawear/presentation/MainActivity.kt | 2 + .../presentation/chat/ChatComponent.kt | 25 ++++++-- .../gigawear/presentation/nav/Child.kt | 2 + .../gigawear/presentation/nav/Route.kt | 4 ++ .../presentation/routing/RoutingComponent.kt | 57 +++++++++++++++++++ .../presentation/routing/RoutingScreen.kt | 18 ++++++ .../updater/presentation/UpdateComponent.kt | 1 + 11 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/HealthCheckImpl.kt create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/LogoutActionImpl.kt create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/domain/HealthCheck.kt create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/routing/RoutingComponent.kt create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/routing/RoutingScreen.kt diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/HealthCheckImpl.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/HealthCheckImpl.kt new file mode 100644 index 0000000..b42c637 --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/HealthCheckImpl.kt @@ -0,0 +1,17 @@ +package ru.shadowsparky.gigawear.data + +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import org.koin.core.annotation.Factory +import ru.shadowsparky.gigawear.domain.HealthCheck + +@Factory +class HealthCheckImpl( + private val httpClient: HttpClient +) : HealthCheck { + override suspend fun healthCheck() { + val response = httpClient.get("$ENDPOINT/healthCheck") + check(response.status == HttpStatusCode.OK) + } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/LogoutActionImpl.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/LogoutActionImpl.kt new file mode 100644 index 0000000..556670b --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/LogoutActionImpl.kt @@ -0,0 +1,10 @@ +package ru.shadowsparky.gigawear.data + +import org.koin.core.annotation.Factory +import ru.shadowsparky.http.domain.LogoutAction +import ru.shadowsparky.http.domain.TokenStorage + +@Factory +class LogoutActionImpl(private val tokenStorage: TokenStorage) : LogoutAction { + override suspend fun logout() { tokenStorage.clear() } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/domain/HealthCheck.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/domain/HealthCheck.kt new file mode 100644 index 0000000..6083988 --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/domain/HealthCheck.kt @@ -0,0 +1,5 @@ +package ru.shadowsparky.gigawear.domain + +interface HealthCheck { + suspend fun healthCheck() +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/GigaWearRootComponent.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/GigaWearRootComponent.kt index 795566d..61f5f08 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/GigaWearRootComponent.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/GigaWearRootComponent.kt @@ -10,7 +10,7 @@ import ru.shadowsparky.ui.nav.RootComponent class GigaWearRootComponent( context: ComponentContext ) : RootComponent(context) { - override val initStack: List = listOf(Route.AuthScreen()) + override val initStack: List = listOf(Route.RoutingScreen()) override val serializer: KSerializer = Route.serializer() override val koin: Koin = ru.shadowsparky.koin } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/MainActivity.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/MainActivity.kt index 94ecd2b..d3b578f 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/MainActivity.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/MainActivity.kt @@ -8,6 +8,7 @@ import org.koin.android.ext.android.inject import ru.shadowsparky.gigawear.presentation.auth.AuthScreen import ru.shadowsparky.gigawear.presentation.chat.ChatScreen import ru.shadowsparky.gigawear.presentation.nav.Child +import ru.shadowsparky.gigawear.presentation.routing.RoutingScreen import ru.shadowsparky.ui.nav.Nav class MainActivity : ComponentActivity() { @@ -21,6 +22,7 @@ class MainActivity : ComponentActivity() { when (child) { is Child.MainChild -> ChatScreen(child.component) is Child.AuthChild -> AuthScreen(child.component) + is Child.RoutingChild -> RoutingScreen(child.component) } } } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatComponent.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatComponent.kt index b409f42..558eb4b 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatComponent.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatComponent.kt @@ -3,6 +3,7 @@ package ru.shadowsparky.gigawear.presentation.chat import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.childContext import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope +import com.arkivanov.essenty.lifecycle.doOnStart import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -19,6 +20,7 @@ import ru.shadowsparky.chat.domain.ChatRoles import ru.shadowsparky.chat.domain.Message import ru.shadowsparky.gigawear.presentation.nav.Child import ru.shadowsparky.gigawear.presentation.nav.Route +import ru.shadowsparky.http.domain.TokenStorage import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.RouteNavigator import ru.shadowsparky.updater.presentation.UpdateComponentFactory @@ -26,7 +28,9 @@ import ru.shadowsparky.updater.presentation.UpdateComponentFactory class ChatComponent( private val context: ComponentContext, private val repository: ChatRepository, - private val updateComponentFactory: UpdateComponentFactory + updateComponentFactory: UpdateComponentFactory, + private val tokenStorage: TokenStorage, + private val navigateToRouting: () -> Unit ) : ComponentContext by context { private val _state = MutableStateFlow(ChatUiState()) val state: StateFlow = _state.asStateFlow() @@ -40,7 +44,16 @@ class ChatComponent( private val _events = Channel(Channel.BUFFERED) val events = _events.receiveAsFlow() - init { updateComponent.checkUpdates() } + init { + lifecycle.doOnStart { + updateComponent.checkUpdates(false) + } + scope.launch { + tokenStorage.token.collect { + if (it == null) navigateToRouting() + } + } + } fun onMicClicked() { if (_state.value.isLoading) return @@ -83,7 +96,8 @@ class ChatComponent( @Named(Route.CHAT) class ChatComponentFactory( private val chatRepository: ChatRepository, - private val updateComponentFactory: UpdateComponentFactory + private val updateComponentFactory: UpdateComponentFactory, + private val tokenStorage: TokenStorage ) : ComponentFactory { override fun create( route: Route.ChatScreen, @@ -93,8 +107,9 @@ class ChatComponentFactory( val component = ChatComponent( child, chatRepository, - updateComponentFactory - ) + updateComponentFactory, + tokenStorage + ) { root.navReplace(Route.RoutingScreen()) } return Child.MainChild(component) } } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Child.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Child.kt index 505e9fa..4845efe 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Child.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Child.kt @@ -2,8 +2,10 @@ package ru.shadowsparky.gigawear.presentation.nav import ru.shadowsparky.gigawear.presentation.auth.AuthComponent import ru.shadowsparky.gigawear.presentation.chat.ChatComponent +import ru.shadowsparky.gigawear.presentation.routing.RoutingComponent sealed class Child : ru.shadowsparky.ui.nav.Child { class MainChild(val component: ChatComponent) : Child() class AuthChild(val component: AuthComponent) : Child() + class RoutingChild(val component: RoutingComponent) : Child() } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Route.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Route.kt index 8fdccfc..cdd4537 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Route.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Route.kt @@ -11,7 +11,11 @@ sealed class Route : ru.shadowsparky.ui.nav.Route() { @Serializable data class AuthScreen(override val routeId: String = AUTH) : Route() + @Serializable + data class RoutingScreen(override val routeId: String = ROUTING) : Route() + companion object { + const val ROUTING = "routing" const val CHAT = "chat" const val AUTH = "auth" } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/routing/RoutingComponent.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/routing/RoutingComponent.kt new file mode 100644 index 0000000..47a9493 --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/routing/RoutingComponent.kt @@ -0,0 +1,57 @@ +package ru.shadowsparky.gigawear.presentation.routing + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Named +import ru.shadowsparky.gigawear.domain.HealthCheck +import ru.shadowsparky.gigawear.presentation.nav.Child +import ru.shadowsparky.gigawear.presentation.nav.Route +import ru.shadowsparky.http.domain.TokenStorage +import ru.shadowsparky.ui.nav.ComponentFactory +import ru.shadowsparky.ui.nav.RouteNavigator + +class RoutingComponent( + private val context: ComponentContext, + private val tokenStorage: TokenStorage, + private val healthCheck: HealthCheck, + private val navigateTo: (Route) -> Unit +) : ComponentContext by context { + private val scope = coroutineScope() + + init { + scope.launch { + if (tokenStorage.token.firstOrNull() != null) { + runCatching { + healthCheck.healthCheck() + navigateTo(Route.ChatScreen()) + }.onFailure { + tokenStorage.clear() + navigateTo(Route.AuthScreen()) + } + } else { + navigateTo(Route.AuthScreen()) + } + } + } +} + +@Factory +@Named(Route.ROUTING) +class RoutingComponentFactory( + private val tokenStorage: TokenStorage, + private val healthCheck: HealthCheck +) : ComponentFactory { + override fun create( + route: Route.RoutingScreen, + child: ComponentContext, + root: RouteNavigator + ): Child.RoutingChild { + val comp = RoutingComponent(child, tokenStorage, healthCheck) { + root.navReplace(it) + } + return Child.RoutingChild(comp) + } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/routing/RoutingScreen.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/routing/RoutingScreen.kt new file mode 100644 index 0000000..32fedd3 --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/routing/RoutingScreen.kt @@ -0,0 +1,18 @@ +package ru.shadowsparky.gigawear.presentation.routing + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.CircularProgressIndicator + +@Composable +fun RoutingScreen(component: RoutingComponent) { + AppScaffold { + Box(Modifier.fillMaxSize()) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + } +} diff --git a/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateComponent.kt b/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateComponent.kt index 114b281..922ca7d 100644 --- a/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateComponent.kt +++ b/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateComponent.kt @@ -49,6 +49,7 @@ class UpdateComponent( fun checkUpdates(byUser: Boolean = true) { if (!hasSupport) return + lastJob?.cancel() lastJob = scope.launch { interactor.checkUpdate().collect { isForceUpdate = (it as? UpdateState.UpdateAvailable)?.isForceUpdate ?: false