add basic updater implementation in gigawear

This commit is contained in:
2026-09-14 16:52:27 +03:00
parent 1c573c4366
commit e77046018d
23 changed files with 230 additions and 90 deletions
+2
View File
@@ -21,3 +21,5 @@ cache
kotlin-js-store kotlin-js-store
release.keystore release.keystore
keystore.properties keystore.properties
tmp
run-gigawear-backend.sh
+1
View File
@@ -21,6 +21,7 @@ dependencies {
implementation(project(":libs:base")) implementation(project(":libs:base"))
implementation(project(":libs:http-client")) implementation(project(":libs:http-client"))
implementation(project(":feature:updater:updater-common")) implementation(project(":feature:updater:updater-common"))
implementation(project(":feature:updater:updater-backend"))
implementation(project(":feature:chat:chat-common")) implementation(project(":feature:chat:chat-common"))
implementation(project(":feature:chat:chat-backend")) implementation(project(":feature:chat:chat-backend"))
@@ -10,8 +10,10 @@ import io.ktor.server.routing.get
import io.ktor.server.routing.routing import io.ktor.server.routing.routing
import org.koin.core.annotation.KoinApplication import org.koin.core.annotation.KoinApplication
import org.koin.plugin.module.dsl.startKoin 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.gigawear.backend.presentation.setupChat
import ru.shadowsparky.koin import ru.shadowsparky.koin
import ru.shadowsparky.updater.backend.presentation.setupUpdates
@KoinApplication @KoinApplication
object GigaWearApp object GigaWearApp
@@ -26,5 +28,6 @@ fun Application.module() {
routing { routing {
get("health") { this.call.respond(HttpStatusCode.OK) } get("health") { this.call.respond(HttpStatusCode.OK) }
setupChat(koin.get()) setupChat(koin.get())
setupUpdates(koin.get()) { MOCK_USER_ID }
} }
} }
@@ -7,7 +7,7 @@ import io.ktor.server.routing.post
import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase import ru.shadowsparky.chat.backend.domain.ProcessUserMessageUseCase
import ru.shadowsparky.chat.domain.Message import ru.shadowsparky.chat.domain.Message
private const val MOCK_USER_ID = -1L const val MOCK_USER_ID = -1L
fun Route.setupChat(useCase: ProcessUserMessageUseCase) { fun Route.setupChat(useCase: ProcessUserMessageUseCase) {
post("chat") { post("chat") {
@@ -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<Message>().copy(timestamp = System.currentTimeMillis())
call.respond(useCase.execute(message, MOCK_USER_ID))
}
}
+2
View File
@@ -30,6 +30,8 @@ dependencies {
implementation(project(":libs:http-client")) implementation(project(":libs:http-client"))
implementation(project(":libs:base")) implementation(project(":libs:base"))
implementation(project(":feature:chat:chat-common")) implementation(project(":feature:chat:chat-common"))
implementation(project(":feature:updater:updater-common"))
implementation(project(":feature:updater:updater-client"))
implementation(libs.activity.compose) implementation(libs.activity.compose)
implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.foundation)
+59
View File
@@ -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.**
@@ -3,8 +3,21 @@ package ru.shadowsparky.gigawear
import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module import org.koin.core.annotation.Module
import org.koin.core.annotation.Singleton
import ru.shadowsparky.domain.AppConfig
@Module @Module
@ComponentScan @ComponentScan
@Configuration @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
}
}
}
@@ -8,13 +8,15 @@ import org.koin.core.annotation.Factory
import ru.shadowsparky.chat.domain.ChatRepository import ru.shadowsparky.chat.domain.ChatRepository
import ru.shadowsparky.chat.domain.Message import ru.shadowsparky.chat.domain.Message
const val ENDPOINT = "http://192.168.0.76:8181"
@Factory @Factory
class GigaChatRepository( class GigaChatRepository(
private val httpClient: HttpClient private val httpClient: HttpClient
) : ChatRepository { ) : ChatRepository {
override suspend fun query(afterId: Long?): List<Message> = emptyList() override suspend fun query(afterId: Long?): List<Message> = emptyList()
override suspend fun send(message: Message): Message { override suspend fun send(message: Message): Message {
return httpClient.post("http://192.168.0.76:8181/chat") { return httpClient.post("$ENDPOINT/chat") {
setBody(message) setBody(message)
}.body<Message>() }.body<Message>()
} }
@@ -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()
}
}
@@ -1,6 +1,7 @@
package ru.shadowsparky.gigawear.presentation package ru.shadowsparky.gigawear.presentation
import android.app.Application import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.core.annotation.KoinApplication import org.koin.core.annotation.KoinApplication
import org.koin.plugin.module.dsl.startKoin import org.koin.plugin.module.dsl.startKoin
@@ -11,6 +12,8 @@ class App : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startKoin<GigaWearApp>() startKoin<GigaWearApp> {
androidContext(this@App)
}
} }
} }
@@ -1,6 +1,7 @@
package ru.shadowsparky.gigawear.presentation.chat package ru.shadowsparky.gigawear.presentation.chat
import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import com.arkivanov.decompose.value.MutableValue import com.arkivanov.decompose.value.MutableValue
import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.update 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.gigawear.presentation.nav.Route
import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator import ru.shadowsparky.ui.nav.RouteNavigator
import ru.shadowsparky.updater.presentation.UpdateComponentFactory
class ChatComponent( class ChatComponent(
private val context: ComponentContext, private val context: ComponentContext,
private val repository: ChatRepository private val repository: ChatRepository,
private val updateComponentFactory: UpdateComponentFactory
) : ComponentContext by context { ) : ComponentContext by context {
private val scope = coroutineScope() private val scope = coroutineScope()
val updateComponent = updateComponentFactory.create(childContext("updater"))
private val _state = MutableValue(ChatUiState()) private val _state = MutableValue(ChatUiState())
val state: Value<ChatUiState> = _state val state: Value<ChatUiState> = _state
@@ -30,6 +34,10 @@ class ChatComponent(
private val _events = Channel<ComponentEvent>(Channel.BUFFERED) private val _events = Channel<ComponentEvent>(Channel.BUFFERED)
val events = _events.receiveAsFlow() val events = _events.receiveAsFlow()
init {
updateComponent.checkUpdates(true)
}
fun onMicClicked() { fun onMicClicked() {
if (_state.value.isLoading) return if (_state.value.isLoading) return
scope.launch { scope.launch {
@@ -75,14 +83,19 @@ class ChatComponent(
@Factory @Factory
@Named(Route.CHAT) @Named(Route.CHAT)
class ChatComponentFactory( class ChatComponentFactory(
private val chatRepository: ChatRepository private val chatRepository: ChatRepository,
private val updateComponentFactory: UpdateComponentFactory
) : ComponentFactory<Child, Route.ChatScreen, Route> { ) : ComponentFactory<Child, Route.ChatScreen, Route> {
override fun create( override fun create(
route: Route.ChatScreen, route: Route.ChatScreen,
child: ComponentContext, child: ComponentContext,
root: RouteNavigator<Route> root: RouteNavigator<Route>
): Child { ): Child {
val component = ChatComponent(child, chatRepository) val component = ChatComponent(
child,
chatRepository,
updateComponentFactory
)
return Child.MainChild(component) return Child.MainChild(component)
} }
} }
@@ -40,6 +40,7 @@ import com.arkivanov.decompose.extensions.compose.subscribeAsState
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import ru.shadowsparky.gigawear.R import ru.shadowsparky.gigawear.R
import ru.shadowsparky.gigawear.presentation.WearErrorContent import ru.shadowsparky.gigawear.presentation.WearErrorContent
import ru.shadowsparky.updater.presentation.UpdateDialog
import java.util.Locale import java.util.Locale
@Composable @Composable
@@ -171,6 +172,7 @@ fun ChatScreen(component: ChatComponent) {
} }
} }
) { contentPadding -> ) { contentPadding ->
UpdateDialog(component.updateComponent)
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
+1
View File
@@ -21,6 +21,7 @@ dependencies {
implementation(project(":libs:base")) implementation(project(":libs:base"))
implementation(project(":libs:http-client")) implementation(project(":libs:http-client"))
implementation(project(":feature:updater:updater-common")) implementation(project(":feature:updater:updater-common"))
implementation(project(":feature:updater:updater-backend"))
implementation(project(":feature:chat:chat-common")) implementation(project(":feature:chat:chat-common"))
implementation(project(":feature:chat:chat-backend")) implementation(project(":feature:chat:chat-backend"))
@@ -1,23 +1,10 @@
package ru.shadowsparky.vbox.backend.presentation.routing 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.Route
import io.ktor.server.routing.get import ru.shadowsparky.updater.backend.data.BackendUpdateFetcherFactory
import io.ktor.server.util.getOrFail import ru.shadowsparky.updater.backend.presentation.setupUpdates
import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcherFactory
import ru.shadowsparky.vbox.backend.presentation.obtainUserId
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
fun Route.setupUpdates(updateFetcherFactory: BackendUpdateFetcherFactory) { fun Route.setupUpdates(updateFetcherFactory: BackendUpdateFetcherFactory) {
get("$DYNAMIC_PREFIX/checkUpdates") { setupUpdates(updateFetcherFactory, "$DYNAMIC_PREFIX/")
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())
}
} }
@@ -0,0 +1,2 @@
/build
/bin
@@ -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"))
}
@@ -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
@@ -1,14 +1,14 @@
package ru.shadowsparky.vbox.backend.data package ru.shadowsparky.updater.backend.data
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.util.cio.readChannel import io.ktor.util.cio.readChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import ru.shadowsparky.backend.data.EnvFetcher import ru.shadowsparky.backend.data.EnvFetcher
import ru.shadowsparky.domain.DispatcherProvider
import ru.shadowsparky.http.domain.HttpException import ru.shadowsparky.http.domain.HttpException
import ru.shadowsparky.updater.domain.UpdateFetcher import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.updater.domain.UpdateInfo import ru.shadowsparky.updater.domain.UpdateInfo
@@ -17,18 +17,16 @@ import java.io.File
@Factory @Factory
class BackendUpdateFetcherFactory( class BackendUpdateFetcherFactory(
private val envFetcher: EnvFetcher, private val envFetcher: EnvFetcher,
private val json: Json, private val json: Json
private val dispatcherProvider: DispatcherProvider
) { ) {
fun create(userId: Long): BackendUpdateFetcher { fun create(userId: Long): BackendUpdateFetcher {
return BackendUpdateFetcher(envFetcher, json, dispatcherProvider, userId) return BackendUpdateFetcher(envFetcher, json, userId)
} }
} }
class BackendUpdateFetcher( class BackendUpdateFetcher(
private val envFetcher: EnvFetcher, private val envFetcher: EnvFetcher,
private val json: Json, private val json: Json,
private val dispatcherProvider: DispatcherProvider,
private val userId: Long private val userId: Long
) : UpdateFetcher { ) : UpdateFetcher {
private val fetchMutex = Mutex() 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") val configPath = envFetcher.get("UPDATE_CONFIG")
if (configPath.isBlank()) { if (configPath.isBlank()) {
UpdateInfo() 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") val updateFilePath = envFetcher.get("UPDATE_FILE")
if (updateFilePath.isBlank()) throw HttpException(HttpStatusCode.NotFound.value, "Update not found") if (updateFilePath.isBlank()) throw HttpException(HttpStatusCode.NotFound.value, "Update not found")
val file = File(updateFilePath) val file = File(updateFilePath)
@@ -68,8 +66,8 @@ class BackendUpdateFetcher(
file file
} }
private companion object { private companion object {
var updateInfo: UpdateInfo? = null var updateInfo: UpdateInfo? = null
var updateFile: File? = null var updateFile: File? = null
} }
} }
@@ -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())
}
}
@@ -20,6 +20,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -42,7 +44,6 @@ import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import ru.shadowsparky.ui.components.AlertContent import ru.shadowsparky.ui.components.AlertContent
import ru.shadowsparky.ui.components.ErrorContent import ru.shadowsparky.ui.components.ErrorContent
import ru.shadowsparky.ui.components.LazyColumn
import ru.shadowsparky.ui.components.Loading import ru.shadowsparky.ui.components.Loading
import ru.shadowsparky.updater.domain.UpdateState import ru.shadowsparky.updater.domain.UpdateState
@@ -128,8 +129,9 @@ private fun UpdateAvailableContent(
focusRequester: FocusRequester, focusRequester: FocusRequester,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollState = rememberScrollState()
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text( Text(
@@ -137,36 +139,26 @@ private fun UpdateAvailableContent(
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) )
LazyColumn(modifier = Modifier.fillMaxWidth()) { Text(
item { text = stringResource(Res.string.updater_changelog_title),
Text( style = MaterialTheme.typography.titleMedium
text = stringResource(Res.string.updater_changelog_title), )
style = MaterialTheme.typography.titleMedium Spacer(modifier = Modifier.height(4.dp))
)
} Text(
item { text = changeLog ?: stringResource(Res.string.updater_changelog_empty),
Spacer(modifier = Modifier.height(4.dp)) style = MaterialTheme.typography.bodyMedium
} )
item { Spacer(modifier = Modifier.height(8.dp))
Text(
text = changeLog ?: stringResource(Res.string.updater_changelog_empty), Button(
style = MaterialTheme.typography.bodyMedium onClick = onDownloadClick,
) modifier = Modifier.focusRequester(focusRequester).fillMaxWidth()
} ) {
item { Text(
Spacer(modifier = Modifier.height(8.dp)) stringResource(Res.string.updater_btn_download),
} style = MaterialTheme.typography.labelLarge
item { )
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, focusRequester: FocusRequester,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollState = rememberScrollState()
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@@ -210,8 +203,9 @@ private fun UpdateDownloadingContent(
totalBytes: Long, totalBytes: Long,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollState = rememberScrollState()
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@@ -241,8 +235,9 @@ private fun UpdateReadyContent(
focusRequester: FocusRequester, focusRequester: FocusRequester,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollState = rememberScrollState()
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@@ -269,8 +264,9 @@ private fun UpdateFinishedContent(
focusRequester: FocusRequester, focusRequester: FocusRequester,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollState = rememberScrollState()
Column( Column(
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth().verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
@@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Button
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -32,12 +34,13 @@ fun ErrorContent(
retry: Boolean = true, retry: Boolean = true,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val scrollState = rememberScrollState()
val requester = remember { FocusRequester() } val requester = remember { FocusRequester() }
val buttonTextRes = if (retry) Res.string.retry else Res.string.ok val buttonTextRes = if (retry) Res.string.retry else Res.string.ok
Column( Column(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.padding(4.dp) modifier = modifier.verticalScroll(scrollState).padding(4.dp)
) { ) {
Text( Text(
text = stringResource(Res.string.error_title), text = stringResource(Res.string.error_title),
+1
View File
@@ -49,6 +49,7 @@ include(":libs:http-client")
include(":feature:video-player") include(":feature:video-player")
include(":feature:updater:updater-client") include(":feature:updater:updater-client")
include(":feature:updater:updater-common") include(":feature:updater:updater-common")
include(":feature:updater:updater-backend")
include(":feature:chat:chat-client") include(":feature:chat:chat-client")
include(":feature:chat:chat-common") include(":feature:chat:chat-common")
include(":feature:chat:chat-backend") include(":feature:chat:chat-backend")