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:backend-base"))
implementation(project(":libs:base")) implementation(project(":libs:base"))
implementation(project(":libs:http-client")) implementation(project(":libs:http-client"))
implementation(project(":libs:updater:updater-common"))
implementation(libs.ktor.server.core.jvm) implementation(libs.ktor.server.core.jvm)
implementation(libs.ktor.server.host.common.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 package ru.shadowsparky.vbox.backend.di
import kotlinx.serialization.json.Json
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import ru.shadowsparky.backend.data.TokenVerifier import ru.shadowsparky.backend.data.TokenVerifier
import ru.shadowsparky.backend.domain.JwtInfo import ru.shadowsparky.backend.domain.JwtInfo
import ru.shadowsparky.backend.domain.LoginVerifier 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.BackendRemoteEventHandler
import ru.shadowsparky.vbox.backend.data.BackendUpdateFetcher
import ru.shadowsparky.vbox.backend.data.auth.AuthTokenRepositoryFactory import ru.shadowsparky.vbox.backend.data.auth.AuthTokenRepositoryFactory
import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory import ru.shadowsparky.vbox.backend.data.tags.MovieTagRepositoryFactory
import ru.shadowsparky.vbox.backend.data.tags.UserTagRepositoryFactory import ru.shadowsparky.vbox.backend.data.tags.UserTagRepositoryFactory
@@ -22,9 +23,9 @@ class RoutingEntryPoint(
val searchFactory: SearchRepositoryFactory, val searchFactory: SearchRepositoryFactory,
val recentlyFactory: RecentlyWatchedRepositoryFactory, val recentlyFactory: RecentlyWatchedRepositoryFactory,
val savedMovieFactory: SavedMovieRepositoryFactory, val savedMovieFactory: SavedMovieRepositoryFactory,
val json: Json,
val movieTagFactory: MovieTagRepositoryFactory, val movieTagFactory: MovieTagRepositoryFactory,
val userTagFactory: UserTagRepositoryFactory val userTagFactory: UserTagRepositoryFactory,
val updateFetcher: UpdateFetcher
) )
@Single @Single
@@ -46,5 +46,6 @@ fun Routing.setupAuthMethods(
setupRecentlyWatched(recentlyFactory) setupRecentlyWatched(recentlyFactory)
setupSavedMovie(savedMovieFactory) setupSavedMovie(savedMovieFactory)
setupTagsRouting(userTagFactory, movieTagFactory) 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:ui"))
implementation(project(":libs:http-client")) implementation(project(":libs:http-client"))
implementation(project(":libs:video-player")) implementation(project(":libs:video-player"))
implementation(project(":libs:updater")) implementation(project(":libs:updater:updater-client"))
debugImplementation(libs.ui.tooling) debugImplementation(libs.ui.tooling)
implementation(libs.ui.tooling.preview) implementation(libs.ui.tooling.preview)
testImplementation(libs.koin.test) testImplementation(libs.koin.test)
+1 -1
View File
@@ -26,7 +26,7 @@ kotlin {
implementation(project(":libs:ui")) implementation(project(":libs:ui"))
implementation(project(":libs:http-client")) implementation(project(":libs:http-client"))
implementation(project(":libs:video-player")) implementation(project(":libs:video-player"))
implementation(project(":libs:updater")) implementation(project(":libs:updater:updater-client"))
} }
androidMain.dependencies { androidMain.dependencies {
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
@@ -2,7 +2,6 @@ package ru.shadowsparky.vbox.shared.data
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import org.koin.core.annotation.Factory import org.koin.core.annotation.Factory
import ru.shadowsparky.domain.AppConfig
import ru.shadowsparky.updater.domain.UpdateFetcher import ru.shadowsparky.updater.domain.UpdateFetcher
import ru.shadowsparky.updater.domain.UpdateInfo import ru.shadowsparky.updater.domain.UpdateInfo
import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX import ru.shadowsparky.vbox.shared.domain.DYNAMIC_PREFIX
@@ -12,14 +11,13 @@ import ru.shadowsparky.vbox.shared.domain.getServerConfiguration
@Factory @Factory
class VBoxUpdateFetcher( class VBoxUpdateFetcher(
private val httpClient: HttpClient, private val httpClient: HttpClient,
private val serverConfigurationProvider: ServerConfigurationRepository, private val serverConfigurationProvider: ServerConfigurationRepository
private val appConfig: AppConfig
) : UpdateFetcher { ) : UpdateFetcher {
override suspend fun fetch(): UpdateInfo { override suspend fun fetch(versionCode: Long): UpdateInfo {
return httpClient.get<UpdateInfo>( return httpClient.get<UpdateInfo>(
"$DYNAMIC_PREFIX/checkUpdates", "$DYNAMIC_PREFIX/checkUpdates",
mapOf( mapOf(
"versionCode" to appConfig.versionCode.toString() UpdateFetcher.ARG_VERSION_CODE to versionCode.toString()
), ),
serverConfigurationProvider.getServerConfiguration() serverConfigurationProvider.getServerConfiguration()
) )
@@ -1,5 +0,0 @@
package ru.shadowsparky.updater.domain
interface UpdateFetcher {
suspend fun fetch(): UpdateInfo
}
@@ -12,6 +12,7 @@ kotlin {
dependencies { dependencies {
implementation(project(":libs:ui")) implementation(project(":libs:ui"))
implementation(project(":libs:base")) implementation(project(":libs:base"))
api(project(":libs:updater:updater-common"))
implementation(libs.ui.tooling.preview) implementation(libs.ui.tooling.preview)
} }
} }
@@ -1,5 +1,8 @@
<resources> <resources>
<string name="updater_title_available">Доступно обновление</string> <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_btn_download">Скачать</string>
<string name="updater_changelog_title">Что нового:</string> <string name="updater_changelog_title">Что нового:</string>
<string name="updater_changelog_empty">Улучшения производительности и исправления ошибок.</string> <string name="updater_changelog_empty">Улучшения производительности и исправления ошибок.</string>
@@ -22,9 +22,9 @@ class UpdateInteractor(
} }
emit(UpdateState.Checking) emit(UpdateState.Checking)
try { try {
val updateInfo = fetcher.fetch()
val serverVersion = updateInfo.versionCode ?: 0L
val currentVersion = appConfig.versionCode ?: 0L val currentVersion = appConfig.versionCode ?: 0L
val updateInfo = fetcher.fetch(currentVersion)
val serverVersion = updateInfo.versionCode ?: 0L
if (serverVersion <= currentVersion) { if (serverVersion <= currentVersion) {
emit(UpdateState.UpToDate) emit(UpdateState.UpToDate)
@@ -1,24 +1,25 @@
package ru.shadowsparky.updater.presentation package ru.shadowsparky.updater.presentation
import androiddev.libs.updater.generated.resources.Res import androiddev.libs.updater.updater_client.generated.resources.Res
import androiddev.libs.updater.generated.resources.updater_btn_download import androiddev.libs.updater.updater_client.generated.resources.updater_btn_close
import androiddev.libs.updater.generated.resources.updater_btn_grant import androiddev.libs.updater.updater_client.generated.resources.updater_btn_download
import androiddev.libs.updater.generated.resources.updater_btn_install import androiddev.libs.updater.updater_client.generated.resources.updater_btn_grant
import androiddev.libs.updater.generated.resources.updater_btn_retry import androiddev.libs.updater.updater_client.generated.resources.updater_btn_install
import androiddev.libs.updater.generated.resources.updater_changelog_empty import androiddev.libs.updater.updater_client.generated.resources.updater_btn_retry
import androiddev.libs.updater.generated.resources.updater_changelog_title import androiddev.libs.updater.updater_client.generated.resources.updater_changelog_empty
import androiddev.libs.updater.generated.resources.updater_desc_permission import androiddev.libs.updater.updater_client.generated.resources.updater_changelog_title
import androiddev.libs.updater.generated.resources.updater_download_progress import androiddev.libs.updater.updater_client.generated.resources.updater_desc_permission
import androiddev.libs.updater.generated.resources.updater_title_available import androiddev.libs.updater.updater_client.generated.resources.updater_download_progress
import androiddev.libs.updater.generated.resources.updater_title_downloading import androiddev.libs.updater.updater_client.generated.resources.updater_title_available
import androiddev.libs.updater.generated.resources.updater_title_error import androiddev.libs.updater.updater_client.generated.resources.updater_title_downloading
import androiddev.libs.updater.generated.resources.updater_title_permission import androiddev.libs.updater.updater_client.generated.resources.updater_title_error
import androiddev.libs.updater.generated.resources.updater_title_ready 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.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
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
@@ -52,7 +53,7 @@ fun UpdateBottomSheet(
confirmValueChange = { state !is UpdateState.UpdateAvailable || !(state as UpdateState.UpdateAvailable).isForceUpdate } 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) { if (isVisible) {
ModalBottomSheet( ModalBottomSheet(
@@ -87,6 +88,9 @@ fun UpdateBottomSheet(
message = currentState.message, message = currentState.message,
onRetryClick = { component.checkUpdates() } onRetryClick = { component.checkUpdates() }
) )
is UpdateState.UpToDate -> {
UpdateNotFoundContent(onSkip = { component.reset() })
}
else -> Unit else -> Unit
} }
} }
@@ -96,7 +100,7 @@ fun UpdateBottomSheet(
@Composable @Composable
private fun UpdateCheckingContent() { private fun UpdateCheckingContent() {
Loading(Modifier.fillMaxSize()) Loading(Modifier.fillMaxWidth())
} }
@Composable @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 @Composable
private fun UpdateErrorContent( private fun UpdateErrorContent(
message: String, 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:ui")
include(":libs:http-client") include(":libs:http-client")
include(":libs:video-player") include(":libs:video-player")
include(":libs:updater") include(":libs:updater:updater-client")
include(":libs:updater:updater-common")