From e77046018d558ca4d13fc943d927c4be8b7d4026 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 14 Sep 2026 16:52:27 +0300 Subject: [PATCH] add basic updater implementation in gigawear --- .gitignore | 2 + apps/giga-wear/backend/build.gradle.kts | 1 + .../ru/shadowsparky/gigawear/backend/Main.kt | 3 + .../backend/presentation/ChatRouting.kt | 2 +- .../backend/presentation/UpdateRouting.kt | 17 ----- apps/giga-wear/client/build.gradle.kts | 4 +- apps/giga-wear/client/proguard-rules.pro | 59 ++++++++++++++++ .../shadowsparky/gigawear/GigaWearModule.kt | 15 +++- .../gigawear/data/GigaChatRepository.kt | 4 +- .../gigawear/data/UpdateFetcherImpl.kt | 18 +++++ .../shadowsparky/gigawear/presentation/App.kt | 5 +- .../presentation/chat/ChatComponent.kt | 19 +++++- .../gigawear/presentation/chat/ChatScreen.kt | 2 + apps/vbox/backend/build.gradle.kts | 1 + .../presentation/routing/UpdateRouting.kt | 19 +----- feature/updater/updater-backend/.gitignore | 2 + .../updater/updater-backend/build.gradle.kts | 14 ++++ .../updater/backend/BackendUpdaterModule.kt | 10 +++ .../backend/data/BackendUpdateFetcher.kt | 22 +++--- .../backend/presentation/UpdateRouting.kt | 27 ++++++++ .../updater/presentation/UpdateDialog.kt | 68 +++++++++---------- .../shadowsparky/ui/components/ErrorInfo.kt | 5 +- settings.gradle.kts | 1 + 23 files changed, 230 insertions(+), 90 deletions(-) delete mode 100644 apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/presentation/UpdateRouting.kt create mode 100644 apps/giga-wear/client/proguard-rules.pro create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/UpdateFetcherImpl.kt create mode 100644 feature/updater/updater-backend/.gitignore create mode 100644 feature/updater/updater-backend/build.gradle.kts create mode 100644 feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/BackendUpdaterModule.kt rename {apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox => feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater}/backend/data/BackendUpdateFetcher.kt (80%) create mode 100644 feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/presentation/UpdateRouting.kt diff --git a/.gitignore b/.gitignore index f7702bf..9118fee 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ cache kotlin-js-store release.keystore keystore.properties +tmp +run-gigawear-backend.sh diff --git a/apps/giga-wear/backend/build.gradle.kts b/apps/giga-wear/backend/build.gradle.kts index d5f48b6..d1e8824 100644 --- a/apps/giga-wear/backend/build.gradle.kts +++ b/apps/giga-wear/backend/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":libs:base")) implementation(project(":libs:http-client")) implementation(project(":feature:updater:updater-common")) + implementation(project(":feature:updater:updater-backend")) implementation(project(":feature:chat:chat-common")) implementation(project(":feature:chat:chat-backend")) diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/Main.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/Main.kt index dd96803..033cc73 100644 --- a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/Main.kt +++ b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/Main.kt @@ -10,8 +10,10 @@ import io.ktor.server.routing.get import io.ktor.server.routing.routing import org.koin.core.annotation.KoinApplication import org.koin.plugin.module.dsl.startKoin +import ru.shadowsparky.gigawear.backend.presentation.MOCK_USER_ID import ru.shadowsparky.gigawear.backend.presentation.setupChat import ru.shadowsparky.koin +import ru.shadowsparky.updater.backend.presentation.setupUpdates @KoinApplication object GigaWearApp @@ -26,5 +28,6 @@ fun Application.module() { routing { get("health") { this.call.respond(HttpStatusCode.OK) } setupChat(koin.get()) + setupUpdates(koin.get()) { MOCK_USER_ID } } } diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/presentation/ChatRouting.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/presentation/ChatRouting.kt index ba65c8e..aab48b3 100644 --- a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/presentation/ChatRouting.kt +++ b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/presentation/ChatRouting.kt @@ -7,7 +7,7 @@ import io.ktor.server.routing.post import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase import ru.shadowsparky.chat.domain.Message -private const val MOCK_USER_ID = -1L +const val MOCK_USER_ID = -1L fun Route.setupChat(useCase: ProcessUserMessageUseCase) { post("chat") { diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/presentation/UpdateRouting.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/presentation/UpdateRouting.kt deleted file mode 100644 index 43b0561..0000000 --- a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/presentation/UpdateRouting.kt +++ /dev/null @@ -1,17 +0,0 @@ -package ru.shadowsparky.gigawear.backend.presentation - -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.routing.Route -import io.ktor.server.routing.Routing -import io.ktor.server.routing.post -import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase -import ru.shadowsparky.chat.domain.Message - -fun Route.setupUpdate(useCase: ProcessUserMessageUseCase) { - post("chat") { - val message = call.receive().copy(timestamp = System.currentTimeMillis()) - call.respond(useCase.execute(message, MOCK_USER_ID)) - } -} - diff --git a/apps/giga-wear/client/build.gradle.kts b/apps/giga-wear/client/build.gradle.kts index 46e62f5..429f88e 100644 --- a/apps/giga-wear/client/build.gradle.kts +++ b/apps/giga-wear/client/build.gradle.kts @@ -30,7 +30,9 @@ dependencies { implementation(project(":libs:http-client")) implementation(project(":libs:base")) implementation(project(":feature:chat:chat-common")) - + implementation(project(":feature:updater:updater-common")) + implementation(project(":feature:updater:updater-client")) + implementation(libs.activity.compose) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.material3) diff --git a/apps/giga-wear/client/proguard-rules.pro b/apps/giga-wear/client/proguard-rules.pro new file mode 100644 index 0000000..8d030c9 --- /dev/null +++ b/apps/giga-wear/client/proguard-rules.pro @@ -0,0 +1,59 @@ +-dontwarn org.bouncycastle.jsse.BCSSLParameters +-dontwarn org.bouncycastle.jsse.BCSSLSocket +-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider +-dontwarn org.conscrypt.Conscrypt$Version +-dontwarn org.conscrypt.Conscrypt +-dontwarn org.conscrypt.ConscryptHostnameVerifier +-dontwarn org.openjsse.javax.net.ssl.SSLParameters +-dontwarn org.openjsse.javax.net.ssl.SSLSocket +-dontwarn org.openjsse.net.ssl.OpenJSSE +-dontwarn org.slf4j.impl.StaticLoggerBinder +-dontwarn java.lang.management.ManagementFactory +-dontwarn java.lang.management.RuntimeMXBean +-dontwarn kotlinx.atomicfu.** +-dontwarn io.netty.** +-dontwarn com.typesafe.** +-dontwarn org.slf4j.** +-dontwarn com.sun.nio.file.SensitivityWatchEventModifier +-dontwarn org.xmlpull.v1.** +-dontwarn javax.xml.namespace.** +-dontwarn androidx.test.platform.app.InstrumentationRegistry +-dontwarn androidx.window.extensions.WindowExtensions +-dontwarn androidx.window.extensions.WindowExtensionsProvider +-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation +-dontwarn androidx.window.extensions.core.util.function.Consumer +-dontwarn androidx.window.extensions.core.util.function.Function +-dontwarn androidx.window.extensions.core.util.function.Predicate +-dontwarn androidx.window.extensions.layout.DisplayFeature +-dontwarn androidx.window.extensions.layout.FoldingFeature +-dontwarn androidx.window.extensions.layout.WindowLayoutComponent +-dontwarn androidx.window.extensions.layout.WindowLayoutInfo +-dontwarn androidx.window.sidecar.SidecarDeviceState +-dontwarn androidx.window.sidecar.SidecarDisplayFeature +-dontwarn androidx.window.sidecar.SidecarInterface$SidecarCallback +-dontwarn androidx.window.sidecar.SidecarInterface +-dontwarn androidx.window.sidecar.SidecarProvider +-dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo + +-keep class ru.shadowsparky.vbox.shared.domain.RemoteEvent { *; } +-keep class ru.shadowsparky.vbox.shared.domain.RemoteEvent$OnSaved { *; } +-keep class ru.shadowsparky.vbox.shared.domain.RemoteEvent$OnRecent { *; } +-keep class ru.shadowsparky.vbox.shared.domain.RemoteEvent$OnSearch { *; } + +-keep class ru.shadowsparky.videobox.multiplatform.**$Config { *; } +-keep class ru.shadowsparky.videobox.multiplatform.**$Configuration { *; } + +-keepclassmembers class * implements android.os.Parcelable { + static ** CREATOR; +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod + +-keep class com.arkivanov.decompose.** { *; } +-keep interface com.arkivanov.decompose.** { *; } +-dontwarn com.arkivanov.decompose.** diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/GigaWearModule.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/GigaWearModule.kt index e736c40..cd1b7f0 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/GigaWearModule.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/GigaWearModule.kt @@ -3,8 +3,21 @@ package ru.shadowsparky.gigawear import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton +import ru.shadowsparky.domain.AppConfig @Module @ComponentScan @Configuration -class GigaWearModule +class GigaWearModule { + + @Singleton + fun provideAppConfig(): AppConfig { + return object : AppConfig { + override val debug = BuildConfig.DEBUG + override val appId = "GigaWearClient" + override val versionCode = BuildConfig.VERSION_CODE.toLong() + override val versionName = BuildConfig.VERSION_NAME + } + } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/GigaChatRepository.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/GigaChatRepository.kt index 018163e..399f4e0 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/GigaChatRepository.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/GigaChatRepository.kt @@ -8,13 +8,15 @@ import org.koin.core.annotation.Factory import ru.shadowsparky.chat.domain.ChatRepository import ru.shadowsparky.chat.domain.Message +const val ENDPOINT = "http://192.168.0.76:8181" + @Factory class GigaChatRepository( private val httpClient: HttpClient ) : ChatRepository { override suspend fun query(afterId: Long?): List = emptyList() override suspend fun send(message: Message): Message { - return httpClient.post("http://192.168.0.76:8181/chat") { + return httpClient.post("$ENDPOINT/chat") { setBody(message) }.body() } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/UpdateFetcherImpl.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/UpdateFetcherImpl.kt new file mode 100644 index 0000000..88743e3 --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/UpdateFetcherImpl.kt @@ -0,0 +1,18 @@ +package ru.shadowsparky.gigawear.data + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import org.koin.core.annotation.Factory +import ru.shadowsparky.updater.domain.UpdateFetcher +import ru.shadowsparky.updater.domain.UpdateInfo + +@Factory +class UpdateFetcherImpl(private val httpClient: HttpClient) : UpdateFetcher { + override suspend fun fetch(versionCode: Long): UpdateInfo { + return httpClient.get("$ENDPOINT/checkUpdates") { + parameter(UpdateFetcher.ARG_VERSION_CODE, versionCode) + }.body() + } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/App.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/App.kt index 4f2cd37..332174a 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/App.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/App.kt @@ -1,6 +1,7 @@ package ru.shadowsparky.gigawear.presentation import android.app.Application +import org.koin.android.ext.koin.androidContext import org.koin.core.annotation.KoinApplication import org.koin.plugin.module.dsl.startKoin @@ -11,6 +12,8 @@ class App : Application() { override fun onCreate() { super.onCreate() - startKoin() + startKoin { + androidContext(this@App) + } } } 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 c9ea45b..a4b1d2a 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 @@ -1,6 +1,7 @@ package ru.shadowsparky.gigawear.presentation.chat import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.childContext import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.update @@ -17,12 +18,15 @@ import ru.shadowsparky.gigawear.presentation.nav.Child import ru.shadowsparky.gigawear.presentation.nav.Route import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.RouteNavigator +import ru.shadowsparky.updater.presentation.UpdateComponentFactory class ChatComponent( private val context: ComponentContext, - private val repository: ChatRepository + private val repository: ChatRepository, + private val updateComponentFactory: UpdateComponentFactory ) : ComponentContext by context { private val scope = coroutineScope() + val updateComponent = updateComponentFactory.create(childContext("updater")) private val _state = MutableValue(ChatUiState()) val state: Value = _state @@ -30,6 +34,10 @@ class ChatComponent( private val _events = Channel(Channel.BUFFERED) val events = _events.receiveAsFlow() + init { + updateComponent.checkUpdates(true) + } + fun onMicClicked() { if (_state.value.isLoading) return scope.launch { @@ -75,14 +83,19 @@ class ChatComponent( @Factory @Named(Route.CHAT) class ChatComponentFactory( - private val chatRepository: ChatRepository + private val chatRepository: ChatRepository, + private val updateComponentFactory: UpdateComponentFactory ) : ComponentFactory { override fun create( route: Route.ChatScreen, child: ComponentContext, root: RouteNavigator ): Child { - val component = ChatComponent(child, chatRepository) + val component = ChatComponent( + child, + chatRepository, + updateComponentFactory + ) return Child.MainChild(component) } } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatScreen.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatScreen.kt index d2eb0bf..63daa62 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatScreen.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatScreen.kt @@ -40,6 +40,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState import kotlinx.coroutines.flow.collectLatest import ru.shadowsparky.gigawear.R import ru.shadowsparky.gigawear.presentation.WearErrorContent +import ru.shadowsparky.updater.presentation.UpdateDialog import java.util.Locale @Composable @@ -171,6 +172,7 @@ fun ChatScreen(component: ChatComponent) { } } ) { contentPadding -> + UpdateDialog(component.updateComponent) LazyColumn( state = listState, modifier = Modifier.fillMaxSize(), diff --git a/apps/vbox/backend/build.gradle.kts b/apps/vbox/backend/build.gradle.kts index 55fd485..ee2849c 100644 --- a/apps/vbox/backend/build.gradle.kts +++ b/apps/vbox/backend/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":libs:base")) implementation(project(":libs:http-client")) implementation(project(":feature:updater:updater-common")) + implementation(project(":feature:updater:updater-backend")) implementation(project(":feature:chat:chat-common")) implementation(project(":feature:chat:chat-backend")) diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/UpdateRouting.kt b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/UpdateRouting.kt index 146604f..1b5813c 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/UpdateRouting.kt +++ b/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/presentation/routing/UpdateRouting.kt @@ -1,23 +1,10 @@ package ru.shadowsparky.vbox.backend.presentation.routing -import io.ktor.server.response.respond -import io.ktor.server.response.respondFile import io.ktor.server.routing.Route -import io.ktor.server.routing.get -import io.ktor.server.util.getOrFail -import ru.shadowsparky.updater.domain.UpdateFetcher -import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcherFactory -import ru.shadowsparky.vbox.backend.presentation.obtainUserId +import ru.shadowsparky.updater.backend.data.BackendUpdateFetcherFactory +import ru.shadowsparky.updater.backend.presentation.setupUpdates import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX fun Route.setupUpdates(updateFetcherFactory: BackendUpdateFetcherFactory) { - get("$DYNAMIC_PREFIX/checkUpdates") { - val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong() - val updateFetcher = updateFetcherFactory.create(call.obtainUserId()) - call.respond(updateFetcher.fetch(versionCode)) - } - get("$DYNAMIC_PREFIX/getApk") { - val updateFetcher = updateFetcherFactory.create(call.obtainUserId()) - call.respondFile(updateFetcher.updateFile()) - } + setupUpdates(updateFetcherFactory, "$DYNAMIC_PREFIX/") } diff --git a/feature/updater/updater-backend/.gitignore b/feature/updater/updater-backend/.gitignore new file mode 100644 index 0000000..2f230b7 --- /dev/null +++ b/feature/updater/updater-backend/.gitignore @@ -0,0 +1,2 @@ +/build +/bin \ No newline at end of file diff --git a/feature/updater/updater-backend/build.gradle.kts b/feature/updater/updater-backend/build.gradle.kts new file mode 100644 index 0000000..b17167c --- /dev/null +++ b/feature/updater/updater-backend/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.convention.jvm) + alias(libs.plugins.convention.ktor.client) + alias(libs.plugins.convention.koin) + alias(libs.plugins.convention.serialization) + alias(libs.plugins.ktor) +} + +dependencies { + implementation(libs.ktor.server.core.jvm) + implementation(project(":libs:backend-base")) + implementation(project(":libs:http-client")) + implementation(project(":feature:updater:updater-common")) +} diff --git a/feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/BackendUpdaterModule.kt b/feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/BackendUpdaterModule.kt new file mode 100644 index 0000000..45ea250 --- /dev/null +++ b/feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/BackendUpdaterModule.kt @@ -0,0 +1,10 @@ +package ru.shadowsparky.updater.backend + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Configuration +import org.koin.core.annotation.Module + +@Module +@ComponentScan +@Configuration +class BackendUpdaterModule diff --git a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendUpdateFetcher.kt b/feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/data/BackendUpdateFetcher.kt similarity index 80% rename from apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendUpdateFetcher.kt rename to feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/data/BackendUpdateFetcher.kt index 05735a9..fd7f7be 100644 --- a/apps/vbox/backend/src/main/kotlin/ru/shadowsparky/vbox/backend/data/BackendUpdateFetcher.kt +++ b/feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/data/BackendUpdateFetcher.kt @@ -1,14 +1,14 @@ -package ru.shadowsparky.vbox.backend.data +package ru.shadowsparky.updater.backend.data import io.ktor.http.HttpStatusCode import io.ktor.util.cio.readChannel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import org.koin.core.annotation.Factory import ru.shadowsparky.backend.data.EnvFetcher -import ru.shadowsparky.domain.DispatcherProvider import ru.shadowsparky.http.domain.HttpException import ru.shadowsparky.updater.domain.UpdateFetcher import ru.shadowsparky.updater.domain.UpdateInfo @@ -17,18 +17,16 @@ import java.io.File @Factory class BackendUpdateFetcherFactory( private val envFetcher: EnvFetcher, - private val json: Json, - private val dispatcherProvider: DispatcherProvider + private val json: Json ) { fun create(userId: Long): BackendUpdateFetcher { - return BackendUpdateFetcher(envFetcher, json, dispatcherProvider, userId) + return BackendUpdateFetcher(envFetcher, json, userId) } } class BackendUpdateFetcher( private val envFetcher: EnvFetcher, private val json: Json, - private val dispatcherProvider: DispatcherProvider, private val userId: Long ) : UpdateFetcher { private val fetchMutex = Mutex() @@ -42,7 +40,7 @@ class BackendUpdateFetcher( } } - private suspend fun fetchInternal(): UpdateInfo = withContext(dispatcherProvider.io) { + private suspend fun fetchInternal(): UpdateInfo = withContext(Dispatchers.IO) { val configPath = envFetcher.get("UPDATE_CONFIG") if (configPath.isBlank()) { UpdateInfo() @@ -59,7 +57,7 @@ class BackendUpdateFetcher( } } - private suspend fun updateFileInternal(): File = withContext(dispatcherProvider.io) { + private suspend fun updateFileInternal(): File = withContext(Dispatchers.IO) { val updateFilePath = envFetcher.get("UPDATE_FILE") if (updateFilePath.isBlank()) throw HttpException(HttpStatusCode.NotFound.value, "Update not found") val file = File(updateFilePath) @@ -68,8 +66,8 @@ class BackendUpdateFetcher( file } - private companion object { - var updateInfo: UpdateInfo? = null - var updateFile: File? = null - } + private companion object { + var updateInfo: UpdateInfo? = null + var updateFile: File? = null + } } diff --git a/feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/presentation/UpdateRouting.kt b/feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/presentation/UpdateRouting.kt new file mode 100644 index 0000000..0638f08 --- /dev/null +++ b/feature/updater/updater-backend/src/main/kotlin/ru/shadowsparky/updater/backend/presentation/UpdateRouting.kt @@ -0,0 +1,27 @@ +package ru.shadowsparky.updater.backend.presentation + +import io.ktor.server.application.ApplicationCall +import io.ktor.server.response.respond +import io.ktor.server.response.respondFile +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.util.getOrFail +import ru.shadowsparky.backend.presentation.obtainUserId +import ru.shadowsparky.updater.backend.data.BackendUpdateFetcherFactory +import ru.shadowsparky.updater.domain.UpdateFetcher + +fun Route.setupUpdates( + updateFetcherFactory: BackendUpdateFetcherFactory, + prefix: String = "", + onUserId: suspend ApplicationCall.() -> Long = { obtainUserId() } +) { + get("${prefix}checkUpdates") { + val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong() + val updateFetcher = updateFetcherFactory.create(onUserId(call)) + call.respond(updateFetcher.fetch(versionCode)) + } + get("${prefix}getApk") { + val updateFetcher = updateFetcherFactory.create(onUserId(call)) + call.respondFile(updateFetcher.updateFile()) + } +} diff --git a/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateDialog.kt b/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateDialog.kt index 5457317..c38d4ee 100644 --- a/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateDialog.kt +++ b/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateDialog.kt @@ -20,6 +20,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -42,7 +44,6 @@ import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import ru.shadowsparky.ui.components.AlertContent import ru.shadowsparky.ui.components.ErrorContent -import ru.shadowsparky.ui.components.LazyColumn import ru.shadowsparky.ui.components.Loading import ru.shadowsparky.updater.domain.UpdateState @@ -128,8 +129,9 @@ private fun UpdateAvailableContent( focusRequester: FocusRequester, modifier: Modifier = Modifier ) { + val scrollState = rememberScrollState() Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth().verticalScroll(scrollState), verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( @@ -137,36 +139,26 @@ private fun UpdateAvailableContent( style = MaterialTheme.typography.titleLarge, modifier = Modifier.align(Alignment.CenterHorizontally) ) - LazyColumn(modifier = Modifier.fillMaxWidth()) { - item { - Text( - text = stringResource(Res.string.updater_changelog_title), - style = MaterialTheme.typography.titleMedium - ) - } - item { - Spacer(modifier = Modifier.height(4.dp)) - } - item { - Text( - text = changeLog ?: stringResource(Res.string.updater_changelog_empty), - style = MaterialTheme.typography.bodyMedium - ) - } - item { - Spacer(modifier = Modifier.height(8.dp)) - } - item { - Button( - onClick = onDownloadClick, - modifier = Modifier.focusRequester(focusRequester).fillMaxWidth() - ) { - Text( - stringResource(Res.string.updater_btn_download), - style = MaterialTheme.typography.labelLarge - ) - } - } + Text( + text = stringResource(Res.string.updater_changelog_title), + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = changeLog ?: stringResource(Res.string.updater_changelog_empty), + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = onDownloadClick, + modifier = Modifier.focusRequester(focusRequester).fillMaxWidth() + ) { + Text( + stringResource(Res.string.updater_btn_download), + style = MaterialTheme.typography.labelLarge + ) } } } @@ -177,8 +169,9 @@ private fun UpdatePermissionContent( focusRequester: FocusRequester, modifier: Modifier = Modifier ) { + val scrollState = rememberScrollState() Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth().verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { @@ -210,8 +203,9 @@ private fun UpdateDownloadingContent( totalBytes: Long, modifier: Modifier = Modifier ) { + val scrollState = rememberScrollState() Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth().verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { @@ -241,8 +235,9 @@ private fun UpdateReadyContent( focusRequester: FocusRequester, modifier: Modifier = Modifier ) { + val scrollState = rememberScrollState() Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth().verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { @@ -269,8 +264,9 @@ private fun UpdateFinishedContent( focusRequester: FocusRequester, modifier: Modifier = Modifier ) { + val scrollState = rememberScrollState() Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth().verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { diff --git a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/ErrorInfo.kt b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/ErrorInfo.kt index 1716786..328dc08 100644 --- a/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/ErrorInfo.kt +++ b/libs/ui/src/commonMain/kotlin/ru/shadowsparky/ui/components/ErrorInfo.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -32,12 +34,13 @@ fun ErrorContent( retry: Boolean = true, onClick: () -> Unit ) { + val scrollState = rememberScrollState() val requester = remember { FocusRequester() } val buttonTextRes = if (retry) Res.string.retry else Res.string.ok Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.padding(4.dp) + modifier = modifier.verticalScroll(scrollState).padding(4.dp) ) { Text( text = stringResource(Res.string.error_title), diff --git a/settings.gradle.kts b/settings.gradle.kts index 289e7dc..d6f9e7f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,7 @@ include(":libs:http-client") include(":feature:video-player") include(":feature:updater:updater-client") include(":feature:updater:updater-common") +include(":feature:updater:updater-backend") include(":feature:chat:chat-client") include(":feature:chat:chat-common") include(":feature:chat:chat-backend")