updater ui, data implementation
This commit is contained in:
@@ -27,6 +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"))
|
||||||
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)
|
||||||
|
|||||||
@@ -26,6 +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"))
|
||||||
}
|
}
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|||||||
@@ -67,4 +67,5 @@
|
|||||||
<string name="user_tags_tag_delete">Удалить</string>
|
<string name="user_tags_tag_delete">Удалить</string>
|
||||||
<string name="movie_tags_empty">Настроить теги</string>
|
<string name="movie_tags_empty">Настроить теги</string>
|
||||||
<string name="user_tags_filter_disable">Не фильтровать</string>
|
<string name="user_tags_filter_disable">Не фильтровать</string>
|
||||||
|
<string name="check_for_updates">Поиск обновлений</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+16
-6
@@ -1,7 +1,7 @@
|
|||||||
package ru.shadowsparky.vbox.shared.presentation.settings
|
package ru.shadowsparky.vbox.shared.presentation.settings
|
||||||
|
|
||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import com.arkivanov.decompose.childContext
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.annotation.Factory
|
import org.koin.core.annotation.Factory
|
||||||
@@ -11,6 +11,7 @@ import ru.shadowsparky.http.domain.LogoutAction
|
|||||||
import ru.shadowsparky.http.domain.TokenStorage
|
import ru.shadowsparky.http.domain.TokenStorage
|
||||||
import ru.shadowsparky.ui.nav.BaseRootComponent
|
import ru.shadowsparky.ui.nav.BaseRootComponent
|
||||||
import ru.shadowsparky.ui.nav.ComponentFactory
|
import ru.shadowsparky.ui.nav.ComponentFactory
|
||||||
|
import ru.shadowsparky.updater.presentation.UpdateComponentFactory
|
||||||
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
import ru.shadowsparky.vbox.shared.di.factory.DispatcherProvider
|
||||||
import ru.shadowsparky.vbox.shared.domain.OfflineStorage
|
import ru.shadowsparky.vbox.shared.domain.OfflineStorage
|
||||||
import ru.shadowsparky.vbox.shared.domain.PlayerSettingsStorge
|
import ru.shadowsparky.vbox.shared.domain.PlayerSettingsStorge
|
||||||
@@ -29,6 +30,7 @@ class SettingsComponent(
|
|||||||
private val playerSettingsStorge: PlayerSettingsStorge,
|
private val playerSettingsStorge: PlayerSettingsStorge,
|
||||||
val offlineSupport: Boolean,
|
val offlineSupport: Boolean,
|
||||||
val isDebug: Boolean,
|
val isDebug: Boolean,
|
||||||
|
updateComponentFactory: UpdateComponentFactory,
|
||||||
val doBack: () -> Unit,
|
val doBack: () -> Unit,
|
||||||
val editServAddr: () -> Unit,
|
val editServAddr: () -> Unit,
|
||||||
val doAuth: () -> Unit,
|
val doAuth: () -> Unit,
|
||||||
@@ -37,9 +39,11 @@ class SettingsComponent(
|
|||||||
val showOffline: () -> Unit,
|
val showOffline: () -> Unit,
|
||||||
val showTags: () -> Unit
|
val showTags: () -> Unit
|
||||||
) : ComponentContext by context {
|
) : ComponentContext by context {
|
||||||
val serverInfo = serverConfigurationRepository.serverConfiguration
|
private val updateComponent = updateComponentFactory.create(childContext("UpdateComponent"))
|
||||||
.filterNotNull()
|
|
||||||
.map { it.asHttpStr() }
|
val hasUpdateSupport = updateComponent.hasSupport
|
||||||
|
|
||||||
|
val serverInfo = serverConfigurationRepository.serverConfiguration.map { it.asHttpStr() }
|
||||||
private val componentScope = createComponentScope()
|
private val componentScope = createComponentScope()
|
||||||
|
|
||||||
val externalPlayerState = playerSettingsStorge.useExternalPlayer
|
val externalPlayerState = playerSettingsStorge.useExternalPlayer
|
||||||
@@ -52,6 +56,10 @@ class SettingsComponent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkUpdates() {
|
||||||
|
updateComponent.checkUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
componentScope.launch(dispatcherProvider.main) {
|
componentScope.launch(dispatcherProvider.main) {
|
||||||
logoutUseCase.logout()
|
logoutUseCase.logout()
|
||||||
@@ -68,7 +76,8 @@ class SettingsComponentFactory(
|
|||||||
private val appConfig: AppConfig,
|
private val appConfig: AppConfig,
|
||||||
private val playerSettingsStorge: PlayerSettingsStorge,
|
private val playerSettingsStorge: PlayerSettingsStorge,
|
||||||
private val tokenStorage: TokenStorage,
|
private val tokenStorage: TokenStorage,
|
||||||
private val offlineStorage: OfflineStorage?
|
private val offlineStorage: OfflineStorage?,
|
||||||
|
private val updateComponentFactory: UpdateComponentFactory
|
||||||
) : ComponentFactory<Child.SettingsChild, Route.Settings, Route> {
|
) : ComponentFactory<Child.SettingsChild, Route.Settings, Route> {
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
@@ -80,7 +89,8 @@ class SettingsComponentFactory(
|
|||||||
child,
|
child,
|
||||||
dispatcherProvider,
|
dispatcherProvider,
|
||||||
serverConfigurationRepository,
|
serverConfigurationRepository,
|
||||||
isDebug = appConfig.isDebug(),
|
isDebug = appConfig.debug,
|
||||||
|
updateComponentFactory = updateComponentFactory,
|
||||||
doBack = { root.onBack() },
|
doBack = { root.onBack() },
|
||||||
editServAddr = { root.nav(Route.Settings.EditServ()) },
|
editServAddr = { root.nav(Route.Settings.EditServ()) },
|
||||||
doAuth = { root.nav(Route.Settings.Auth(required = true)) },
|
doAuth = { root.nav(Route.Settings.Auth(required = true)) },
|
||||||
|
|||||||
+9
@@ -4,6 +4,7 @@ import androiddev.apps.vbox.client.shared.generated.resources.Res
|
|||||||
import androiddev.apps.vbox.client.shared.generated.resources.account_auth
|
import androiddev.apps.vbox.client.shared.generated.resources.account_auth
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.account_logout_auth
|
import androiddev.apps.vbox.client.shared.generated.resources.account_logout_auth
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.account_reg
|
import androiddev.apps.vbox.client.shared.generated.resources.account_reg
|
||||||
|
import androiddev.apps.vbox.client.shared.generated.resources.check_for_updates
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.custom_serv_addr
|
import androiddev.apps.vbox.client.shared.generated.resources.custom_serv_addr
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.edit_password
|
import androiddev.apps.vbox.client.shared.generated.resources.edit_password
|
||||||
import androiddev.apps.vbox.client.shared.generated.resources.external_player_title
|
import androiddev.apps.vbox.client.shared.generated.resources.external_player_title
|
||||||
@@ -72,6 +73,14 @@ fun SettingsContent(comp: SettingsComponent, paddingValues: PaddingValues) {
|
|||||||
modifier = Modifier.clickable { comp.showTags() }
|
modifier = Modifier.clickable { comp.showTags() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (comp.hasUpdateSupport) {
|
||||||
|
item {
|
||||||
|
TextPreference(
|
||||||
|
text = stringResource(Res.string.check_for_updates),
|
||||||
|
modifier = Modifier.clickable { comp.checkUpdates() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
val needAuth = tokenInfo == null
|
val needAuth = tokenInfo == null
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ kotlin-dsl-version = "5.1.2"
|
|||||||
coreKtx = "1.19.0"
|
coreKtx = "1.19.0"
|
||||||
workRuntimeKtx = "2.11.2"
|
workRuntimeKtx = "2.11.2"
|
||||||
krpc = "0.10.3"
|
krpc = "0.10.3"
|
||||||
|
core = "1.19.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -139,6 +140,7 @@ kotlinx-rpc-krpc-ktor-server = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krp
|
|||||||
kotlinx-rpc-krpc-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json", version.ref = "krpc" }
|
kotlinx-rpc-krpc-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json", version.ref = "krpc" }
|
||||||
kotlinx-rpc-krpc-server = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-server", version.ref = "krpc" }
|
kotlinx-rpc-krpc-server = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-server", version.ref = "krpc" }
|
||||||
kotlinx-rpc-krpc-client = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-client", version.ref = "krpc" }
|
kotlinx-rpc-krpc-client = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-client", version.ref = "krpc" }
|
||||||
|
androidx-core = { group = "androidx.core", name = "core", version.ref = "core" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-gradle-plugin" }
|
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-gradle-plugin" }
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ kotlin {
|
|||||||
implementation(libs.ui.tooling.preview)
|
implementation(libs.ui.tooling.preview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val androidMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.androidx.core)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
android { namespace = "ru.shadowsparky.updater" }
|
android { namespace = "ru.shadowsparky.updater" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"
|
||||||
|
tools:ignore="RequestInstallPackagesPolicy" />
|
||||||
|
</manifest>
|
||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package ru.shadowsparky.updater.data
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import org.koin.core.annotation.Factory
|
||||||
|
import ru.shadowsparky.updater.domain.UpdateInstaller
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Factory
|
||||||
|
class AndroidUpdateInstaller(private val app: Application) : UpdateInstaller {
|
||||||
|
|
||||||
|
override suspend fun hasInstallPermission(): Boolean {
|
||||||
|
return app.packageManager.canRequestPackageInstalls()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun requestInstallPermission() {
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
||||||
|
data = "package:${app.packageName}".toUri()
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
app.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun install(path: String) {
|
||||||
|
val file = File(path)
|
||||||
|
if (!file.exists()) return
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
val uri: Uri = FileProvider.getUriForFile(
|
||||||
|
app,
|
||||||
|
"${app.packageName}.provider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
intent.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
app.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
package ru.shadowsparky.updater.data
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsChannel
|
||||||
|
import io.ktor.http.contentLength
|
||||||
|
import io.ktor.utils.io.ByteReadChannel
|
||||||
|
import io.ktor.utils.io.readAvailable
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import org.koin.core.annotation.Factory
|
||||||
|
import ru.shadowsparky.updater.domain.DownloadProgress
|
||||||
|
import ru.shadowsparky.updater.domain.UpdateDownloader
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Factory
|
||||||
|
class KtorUpdateDownloader(
|
||||||
|
private val httpClient: HttpClient,
|
||||||
|
app: Application,
|
||||||
|
) : UpdateDownloader {
|
||||||
|
private val destinationPath = "${app.cacheDir.absolutePath}/update"
|
||||||
|
|
||||||
|
override fun download(url: String): Flow<DownloadProgress> = flow {
|
||||||
|
try {
|
||||||
|
val response = httpClient.get(url)
|
||||||
|
val contentLength = response.contentLength() ?: -1L
|
||||||
|
val channel: ByteReadChannel = response.bodyAsChannel()
|
||||||
|
|
||||||
|
val directory = File(destinationPath)
|
||||||
|
if (!directory.exists()) {
|
||||||
|
directory.mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileName = "update.apk"
|
||||||
|
val fullPath = "$destinationPath/$fileName"
|
||||||
|
val file = File(fullPath)
|
||||||
|
|
||||||
|
if (file.exists()) file.delete()
|
||||||
|
|
||||||
|
val bufferSize = 8192
|
||||||
|
val buffer = ByteArray(bufferSize)
|
||||||
|
var downloadedBytes = 0L
|
||||||
|
|
||||||
|
file.outputStream().use { outputStream ->
|
||||||
|
while (!channel.isClosedForRead) {
|
||||||
|
val read = channel.readAvailable(buffer, 0, buffer.size)
|
||||||
|
if (read <= 0) break
|
||||||
|
|
||||||
|
outputStream.write(buffer, 0, read)
|
||||||
|
downloadedBytes += read
|
||||||
|
|
||||||
|
if (contentLength > 0) {
|
||||||
|
val progress = downloadedBytes.toFloat() / contentLength.toFloat()
|
||||||
|
emit(
|
||||||
|
DownloadProgress.Active(
|
||||||
|
progress = progress,
|
||||||
|
downloadedBytes = downloadedBytes,
|
||||||
|
totalBytes = contentLength
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(DownloadProgress.Success(path = file.absolutePath))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emit(DownloadProgress.Error(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path name="updater_isolated_cache" path="update/" />
|
||||||
|
</paths>
|
||||||
@@ -12,6 +12,9 @@ class UpdateInteractor(
|
|||||||
private val installer: UpdateInstaller?,
|
private val installer: UpdateInstaller?,
|
||||||
private val appConfig: AppConfig?
|
private val appConfig: AppConfig?
|
||||||
) {
|
) {
|
||||||
|
val hasSupport = fetcher != null && downloader != null && installer != null &&
|
||||||
|
appConfig?.versionCode != null
|
||||||
|
|
||||||
fun checkUpdate(): Flow<UpdateState> = flow {
|
fun checkUpdate(): Flow<UpdateState> = flow {
|
||||||
if (appConfig?.versionCode == null || fetcher == null) {
|
if (appConfig?.versionCode == null || fetcher == null) {
|
||||||
emit(UpdateState.UpToDate)
|
emit(UpdateState.UpToDate)
|
||||||
|
|||||||
+6
-6
@@ -90,7 +90,7 @@ fun UpdateBottomSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateCheckingContent(modifier: Modifier = Modifier) {
|
private fun UpdateCheckingContent(modifier: Modifier = Modifier) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
@@ -98,7 +98,7 @@ fun UpdateCheckingContent(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateAvailableContent(
|
private fun UpdateAvailableContent(
|
||||||
versionName: String,
|
versionName: String,
|
||||||
changeLog: String?,
|
changeLog: String?,
|
||||||
onDownloadClick: () -> Unit,
|
onDownloadClick: () -> Unit,
|
||||||
@@ -131,7 +131,7 @@ fun UpdateAvailableContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdatePermissionContent(
|
private fun UpdatePermissionContent(
|
||||||
onGrantClick: () -> Unit,
|
onGrantClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -149,7 +149,7 @@ fun UpdatePermissionContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateDownloadingContent(
|
private fun UpdateDownloadingContent(
|
||||||
progress: Float,
|
progress: Float,
|
||||||
downloadedBytes: Long,
|
downloadedBytes: Long,
|
||||||
totalBytes: Long,
|
totalBytes: Long,
|
||||||
@@ -174,7 +174,7 @@ fun UpdateDownloadingContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateReadyContent(
|
private fun UpdateReadyContent(
|
||||||
onInstallClick: () -> Unit,
|
onInstallClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -191,7 +191,7 @@ fun UpdateReadyContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpdateErrorContent(
|
private fun UpdateErrorContent(
|
||||||
message: String,
|
message: String,
|
||||||
onRetryClick: () -> Unit,
|
onRetryClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|||||||
+10
-1
@@ -10,13 +10,15 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.annotation.Factory
|
||||||
import ru.shadowsparky.updater.domain.UpdateInteractor
|
import ru.shadowsparky.updater.domain.UpdateInteractor
|
||||||
import ru.shadowsparky.updater.domain.UpdateState
|
import ru.shadowsparky.updater.domain.UpdateState
|
||||||
|
|
||||||
class UpdateComponent(
|
class UpdateComponent(
|
||||||
componentContext: ComponentContext,
|
componentContext: ComponentContext,
|
||||||
private val interactor: UpdateInteractor,
|
private val interactor: UpdateInteractor
|
||||||
) : ComponentContext by componentContext {
|
) : ComponentContext by componentContext {
|
||||||
|
val hasSupport = interactor.hasSupport
|
||||||
|
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||||
|
|
||||||
@@ -64,3 +66,10 @@ class UpdateComponent(
|
|||||||
scope.launch { interactor.installApk(path) }
|
scope.launch { interactor.installApk(path) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Factory
|
||||||
|
class UpdateComponentFactory(private val interactor: UpdateInteractor) {
|
||||||
|
fun create(componentContext: ComponentContext): UpdateComponent {
|
||||||
|
return UpdateComponent(componentContext, interactor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user