add routing screen

This commit is contained in:
2026-09-15 10:59:24 +03:00
parent aeb687fc9d
commit 4c6735746e
11 changed files with 137 additions and 6 deletions
@@ -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)
}
}
@@ -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() }
}
@@ -0,0 +1,5 @@
package ru.shadowsparky.gigawear.domain
interface HealthCheck {
suspend fun healthCheck()
}
@@ -10,7 +10,7 @@ import ru.shadowsparky.ui.nav.RootComponent
class GigaWearRootComponent(
context: ComponentContext
) : RootComponent<Route>(context) {
override val initStack: List<Route> = listOf(Route.AuthScreen())
override val initStack: List<Route> = listOf(Route.RoutingScreen())
override val serializer: KSerializer<Route> = Route.serializer()
override val koin: Koin = ru.shadowsparky.koin
}
@@ -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)
}
}
}
@@ -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<ChatUiState> = _state.asStateFlow()
@@ -40,7 +44,16 @@ class ChatComponent(
private val _events = Channel<ComponentEvent>(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<Child, Route.ChatScreen, Route> {
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)
}
}
@@ -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()
}
@@ -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"
}
@@ -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<Child.RoutingChild, Route.RoutingScreen, Route> {
override fun create(
route: Route.RoutingScreen,
child: ComponentContext,
root: RouteNavigator<Route>
): Child.RoutingChild {
val comp = RoutingComponent(child, tokenStorage, healthCheck) {
root.navReplace(it)
}
return Child.RoutingChild(comp)
}
}
@@ -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))
}
}
}