backend implementation

This commit is contained in:
2026-07-25 21:54:24 +03:00
parent c49a5ed087
commit 9254829c5d
28 changed files with 130 additions and 35 deletions
+1
View File
@@ -20,6 +20,7 @@ dependencies {
implementation(project(":libs:backend-base"))
implementation(project(":libs:base"))
implementation(project(":libs:http-client"))
implementation(project(":libs:updater:updater-common"))
implementation(libs.ktor.server.core.jvm)
implementation(libs.ktor.server.host.common.jvm)
@@ -0,0 +1,31 @@
package ru.shadowsparky.vbox.backend.data
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.updater.domain.UpdateFetcher
import ru.shadowsparky.updater.domain.UpdateInfo
import java.io.File
@Factory
class BackendUpdateFetcher(
private val envFetcher: EnvFetcher,
private val json: Json,
private val dispatcherProvider: DispatcherProvider
) : UpdateFetcher {
private var config: UpdateInfo? = null
override suspend fun fetch(versionCode: Long): UpdateInfo = withContext(dispatcherProvider.io) {
config?.let { return@withContext it }
val configPath = envFetcher.get("UPDATE_CONFIG")
if (configPath.isBlank()) {
UpdateInfo()
} else {
json.decodeFromString<UpdateInfo>(File(configPath).readText()).apply {
config = this
}
}
}
}
@@ -1,12 +1,13 @@
package ru.shadowsparky.vbox.backend.di
import kotlinx.serialization.json.Json
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import ru.shadowsparky.backend.data.TokenVerifier
import ru.shadowsparky.backend.domain.JwtInfo
import ru.shadowsparky.backend.domain.LoginVerifier
import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.vbox.backend.data.BackendRemoteEventHandler
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcher
import ru.shadowsparky.vbox.backend.data.auth.AuthTokenRepositoryFactory
import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory
import ru.shadowsparky.vbox.backend.data.tags.UserTagRepositoryFactory
@@ -22,9 +23,9 @@ class RoutingEntryPoint(
val searchFactory: SearchRepositoryFactory,
val recentlyFactory: RecentlyWatchedRepositoryFactory,
val savedMovieFactory: SavedMovieRepositoryFactory,
val json: Json,
val movieTagFactory: MovieTagRepositoryFactory,
val userTagFactory: UserTagRepositoryFactory
val userTagFactory: UserTagRepositoryFactory,
val updateFetcher: UpdateFetcher
)
@Single
@@ -46,5 +46,6 @@ fun Routing.setupAuthMethods(
setupRecentlyWatched(recentlyFactory)
setupSavedMovie(savedMovieFactory)
setupTagsRouting(userTagFactory, movieTagFactory)
setupUpdates(updateFetcher)
}
}
@@ -0,0 +1,15 @@
package ru.shadowsparky.vbox.backend.presentation.routing
import io.ktor.server.response.respond
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import io.ktor.server.util.getOrFail
import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
fun Routing.setupUpdates(updateFetcher: UpdateFetcher) {
get("$DYNAMIC_PREFIX/checkUpdates") {
val versionCode = call.parameters.getOrFail(UpdateFetcher.ARG_VERSION_CODE).toLong()
call.respond(updateFetcher.fetch(versionCode))
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ dependencies {
implementation(project(":libs:ui"))
implementation(project(":libs:http-client"))
implementation(project(":libs:video-player"))
implementation(project(":libs:updater"))
implementation(project(":libs:updater:updater-client"))
debugImplementation(libs.ui.tooling)
implementation(libs.ui.tooling.preview)
testImplementation(libs.koin.test)
+1 -1
View File
@@ -26,7 +26,7 @@ kotlin {
implementation(project(":libs:ui"))
implementation(project(":libs:http-client"))
implementation(project(":libs:video-player"))
implementation(project(":libs:updater"))
implementation(project(":libs:updater:updater-client"))
}
androidMain.dependencies {
implementation(libs.androidx.appcompat)
@@ -2,7 +2,6 @@ package ru.shadowsparky.vbox.shared.data
import io.ktor.client.HttpClient
import org.koin.core.annotation.Factory
import ru.shadowsparky.domain.AppConfig
import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.updater.domain.UpdateInfo
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
@@ -12,14 +11,13 @@ import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
@Factory
class VBoxUpdateFetcher(
private val httpClient: HttpClient,
private val serverConfigurationProvider: ServerConfigurationRepository,
private val appConfig: AppConfig
private val serverConfigurationProvider: ServerConfigurationRepository
) : UpdateFetcher {
override suspend fun fetch(): UpdateInfo {
override suspend fun fetch(versionCode: Long): UpdateInfo {
return httpClient.get<UpdateInfo>(
"$DYNAMIC_PREFIX/checkUpdates",
mapOf(
"versionCode" to appConfig.versionCode.toString()
UpdateFetcher.ARG_VERSION_CODE to versionCode.toString()
),
serverConfigurationProvider.getServerConfiguration()
)
@@ -1,5 +0,0 @@
package ru.shadowsparky.updater.domain
interface UpdateFetcher {
suspend fun fetch(): UpdateInfo
}
@@ -12,6 +12,7 @@ kotlin {
dependencies {
implementation(project(":libs:ui"))
implementation(project(":libs:base"))
api(project(":libs:updater:updater-common"))
implementation(libs.ui.tooling.preview)
}
}
@@ -1,5 +1,8 @@
<resources>
<string name="updater_title_available">Доступно обновление</string>
<string name="updater_title_not_found">Обновления не найдены</string>
<string name="updater_btn_close">Закрыть</string>
<string name="updater_btn_download">Скачать</string>
<string name="updater_changelog_title">Что нового:</string>
<string name="updater_changelog_empty">Улучшения производительности и исправления ошибок.</string>
@@ -22,9 +22,9 @@ class UpdateInteractor(
}
emit(UpdateState.Checking)
try {
val updateInfo = fetcher.fetch()
val serverVersion = updateInfo.versionCode ?: 0L
val currentVersion = appConfig.versionCode ?: 0L
val updateInfo = fetcher.fetch(currentVersion)
val serverVersion = updateInfo.versionCode ?: 0L
if (serverVersion <= currentVersion) {
emit(UpdateState.UpToDate)
@@ -1,24 +1,25 @@
package ru.shadowsparky.updater.presentation
import androiddev.libs.updater.generated.resources.Res
import androiddev.libs.updater.generated.resources.updater_btn_download
import androiddev.libs.updater.generated.resources.updater_btn_grant
import androiddev.libs.updater.generated.resources.updater_btn_install
import androiddev.libs.updater.generated.resources.updater_btn_retry
import androiddev.libs.updater.generated.resources.updater_changelog_empty
import androiddev.libs.updater.generated.resources.updater_changelog_title
import androiddev.libs.updater.generated.resources.updater_desc_permission
import androiddev.libs.updater.generated.resources.updater_download_progress
import androiddev.libs.updater.generated.resources.updater_title_available
import androiddev.libs.updater.generated.resources.updater_title_downloading
import androiddev.libs.updater.generated.resources.updater_title_error
import androiddev.libs.updater.generated.resources.updater_title_permission
import androiddev.libs.updater.generated.resources.updater_title_ready
import androiddev.libs.updater.updater_client.generated.resources.Res
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_close
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_download
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_grant
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_install
import androiddev.libs.updater.updater_client.generated.resources.updater_btn_retry
import androiddev.libs.updater.updater_client.generated.resources.updater_changelog_empty
import androiddev.libs.updater.updater_client.generated.resources.updater_changelog_title
import androiddev.libs.updater.updater_client.generated.resources.updater_desc_permission
import androiddev.libs.updater.updater_client.generated.resources.updater_download_progress
import androiddev.libs.updater.updater_client.generated.resources.updater_title_available
import androiddev.libs.updater.updater_client.generated.resources.updater_title_downloading
import androiddev.libs.updater.updater_client.generated.resources.updater_title_error
import androiddev.libs.updater.updater_client.generated.resources.updater_title_not_found
import androiddev.libs.updater.updater_client.generated.resources.updater_title_permission
import androiddev.libs.updater.updater_client.generated.resources.updater_title_ready
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -52,7 +53,7 @@ fun UpdateBottomSheet(
confirmValueChange = { state !is UpdateState.UpdateAvailable || !(state as UpdateState.UpdateAvailable).isForceUpdate }
)
val isVisible = remember(state) { state !is UpdateState.Idle && state !is UpdateState.UpToDate }
val isVisible = remember(state) { state !is UpdateState.Idle }
if (isVisible) {
ModalBottomSheet(
@@ -87,6 +88,9 @@ fun UpdateBottomSheet(
message = currentState.message,
onRetryClick = { component.checkUpdates() }
)
is UpdateState.UpToDate -> {
UpdateNotFoundContent(onSkip = { component.reset() })
}
else -> Unit
}
}
@@ -96,7 +100,7 @@ fun UpdateBottomSheet(
@Composable
private fun UpdateCheckingContent() {
Loading(Modifier.fillMaxSize())
Loading(Modifier.fillMaxWidth())
}
@Composable
@@ -192,6 +196,23 @@ private fun UpdateReadyContent(
}
}
@Composable
private fun UpdateNotFoundContent(
onSkip: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(text = stringResource(Res.string.updater_title_not_found), style = MaterialTheme.typography.titleLarge)
Button(onClick = onSkip, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(Res.string.updater_btn_close), style = MaterialTheme.typography.labelLarge)
}
}
}
@Composable
private fun UpdateErrorContent(
message: String,
+2
View File
@@ -0,0 +1,2 @@
/build
/bin
@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.convention.kmp)
alias(libs.plugins.convention.android.lib)
alias(libs.plugins.convention.serialization)
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":libs:base"))
}
}
}
android { namespace = "ru.shadowsparky.updater.common" }
}
@@ -0,0 +1,9 @@
package ru.shadowsparky.updater.domain
interface UpdateFetcher {
suspend fun fetch(versionCode: Long): UpdateInfo
companion object {
const val ARG_VERSION_CODE = "versionCode"
}
}
+2 -1
View File
@@ -43,4 +43,5 @@ include(":libs:base")
include(":libs:ui")
include(":libs:http-client")
include(":libs:video-player")
include(":libs:updater")
include(":libs:updater:updater-client")
include(":libs:updater:updater-common")