From 35584f770d4794227c96ecf0ccd866de266296c5 Mon Sep 17 00:00:00 2001 From: ShiftHackZ Date: Sat, 5 Apr 2025 21:07:53 +0300 Subject: [PATCH 1/3] Local Model Download mirrors --- .../src/main/res/values/strings.xml | 1 + .../DownloadableModelRepositoryImpl.kt | 6 +- .../aisdv1/domain/di/DomainModule.kt | 3 + .../repository/DownloadableModelRepository.kt | 2 +- .../downloadable/DownloadModelUseCase.kt | 2 +- .../downloadable/DownloadModelUseCaseImpl.kt | 2 +- .../downloadable/GetLocalModelUseCase.kt | 8 + .../downloadable/GetLocalModelUseCaseImpl.kt | 12 + .../aisdv1/presentation/di/ViewModelModule.kt | 2 + .../presentation/modal/ModalRenderer.kt | 10 + .../modal/download/DownloadDialog.kt | 215 ++++++++++++++++++ .../modal/download/DownloadDialogEffect.kt | 10 + .../modal/download/DownloadDialogIntent.kt | 14 ++ .../modal/download/DownloadDialogState.kt | 13 ++ .../modal/download/DownloadDialogViewModel.kt | 42 ++++ .../aisdv1/presentation/model/Modal.kt | 2 + .../screen/setup/ServerSetupIntent.kt | 6 +- .../screen/setup/ServerSetupViewModel.kt | 84 ++++--- .../src/main/res/drawable/ic_github.xml | 10 + 19 files changed, 396 insertions(+), 48 deletions(-) create mode 100644 domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/GetLocalModelUseCase.kt create mode 100644 domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/GetLocalModelUseCaseImpl.kt create mode 100644 presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialog.kt create mode 100644 presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogEffect.kt create mode 100644 presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogIntent.kt create mode 100644 presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogState.kt create mode 100644 presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogViewModel.kt create mode 100644 presentation/src/main/res/drawable/ic_github.xml diff --git a/core/localization/src/main/res/values/strings.xml b/core/localization/src/main/res/values/strings.xml index 8263cc8f8..c767358e6 100755 --- a/core/localization/src/main/res/values/strings.xml +++ b/core/localization/src/main/res/values/strings.xml @@ -186,6 +186,7 @@ Textual Inversion Inversion Edit tag + Select source You have %1$s photos saved in Download/SDAI Created diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/repository/DownloadableModelRepositoryImpl.kt b/data/src/main/java/com/shifthackz/aisdv1/data/repository/DownloadableModelRepositoryImpl.kt index 6f84189c3..0e46f816c 100644 --- a/data/src/main/java/com/shifthackz/aisdv1/data/repository/DownloadableModelRepositoryImpl.kt +++ b/data/src/main/java/com/shifthackz/aisdv1/data/repository/DownloadableModelRepositoryImpl.kt @@ -13,11 +13,7 @@ internal class DownloadableModelRepositoryImpl( private val buildInfoProvider: BuildInfoProvider, ) : DownloadableModelRepository { - override fun download(id: String) = localDataSource - .getById(id) - .flatMapObservable { model -> - remoteDataSource.download(id, model.sources.firstOrNull() ?: "") - } + override fun download(id: String, url: String) = remoteDataSource.download(id, url) override fun delete(id: String) = localDataSource.delete(id) diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/di/DomainModule.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/di/DomainModule.kt index 0dda2ed46..3a5f59eb0 100755 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/di/DomainModule.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/di/DomainModule.kt @@ -38,6 +38,8 @@ import com.shifthackz.aisdv1.domain.usecase.downloadable.DownloadModelUseCase import com.shifthackz.aisdv1.domain.usecase.downloadable.DownloadModelUseCaseImpl import com.shifthackz.aisdv1.domain.usecase.downloadable.GetLocalMediaPipeModelsUseCase import com.shifthackz.aisdv1.domain.usecase.downloadable.GetLocalMediaPipeModelsUseCaseImpl +import com.shifthackz.aisdv1.domain.usecase.downloadable.GetLocalModelUseCase +import com.shifthackz.aisdv1.domain.usecase.downloadable.GetLocalModelUseCaseImpl import com.shifthackz.aisdv1.domain.usecase.downloadable.GetLocalOnnxModelsUseCase import com.shifthackz.aisdv1.domain.usecase.downloadable.GetLocalOnnxModelsUseCaseImpl import com.shifthackz.aisdv1.domain.usecase.downloadable.ObserveLocalOnnxModelsUseCase @@ -185,6 +187,7 @@ internal val useCasesModule = module { factoryOf(::FetchAndGetStabilityAiEnginesUseCaseImpl) bind FetchAndGetStabilityAiEnginesUseCase::class factoryOf(::FetchAndGetSupportersUseCaseImpl) bind FetchAndGetSupportersUseCase::class factoryOf(::SendReportUseCaseImpl) bind SendReportUseCase::class + factoryOf(::GetLocalModelUseCaseImpl) bind GetLocalModelUseCase::class } internal val interActorsModule = module { diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/DownloadableModelRepository.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/DownloadableModelRepository.kt index 79efed665..9d124726e 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/DownloadableModelRepository.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/DownloadableModelRepository.kt @@ -8,7 +8,7 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single interface DownloadableModelRepository { - fun download(id: String): Observable + fun download(id: String, url: String): Observable fun delete(id: String): Completable fun getAllOnnx(): Single> fun getAllMediaPipe(): Single> diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCase.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCase.kt index 331fa2438..a956f3c6c 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCase.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCase.kt @@ -4,5 +4,5 @@ import com.shifthackz.aisdv1.domain.entity.DownloadState import io.reactivex.rxjava3.core.Observable interface DownloadModelUseCase { - operator fun invoke(id: String): Observable + operator fun invoke(id: String, url: String): Observable } diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCaseImpl.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCaseImpl.kt index 6c5cf2925..d9d326332 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCaseImpl.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCaseImpl.kt @@ -6,5 +6,5 @@ internal class DownloadModelUseCaseImpl( private val downloadableModelRepository: DownloadableModelRepository, ) : DownloadModelUseCase { - override fun invoke(id: String) = downloadableModelRepository.download(id) + override fun invoke(id: String, url: String) = downloadableModelRepository.download(id, url) } diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/GetLocalModelUseCase.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/GetLocalModelUseCase.kt new file mode 100644 index 000000000..4bf4b43d3 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/GetLocalModelUseCase.kt @@ -0,0 +1,8 @@ +package com.shifthackz.aisdv1.domain.usecase.downloadable + +import com.shifthackz.aisdv1.domain.entity.LocalAiModel +import io.reactivex.rxjava3.core.Single + +interface GetLocalModelUseCase { + operator fun invoke(id: String): Single +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/GetLocalModelUseCaseImpl.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/GetLocalModelUseCaseImpl.kt new file mode 100644 index 000000000..3b6c6188f --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/downloadable/GetLocalModelUseCaseImpl.kt @@ -0,0 +1,12 @@ +package com.shifthackz.aisdv1.domain.usecase.downloadable + +import com.shifthackz.aisdv1.domain.datasource.DownloadableModelDataSource +import com.shifthackz.aisdv1.domain.entity.LocalAiModel +import io.reactivex.rxjava3.core.Single + +internal class GetLocalModelUseCaseImpl( + private val localDataSource: DownloadableModelDataSource.Local, +) : GetLocalModelUseCase { + + override fun invoke(id: String): Single = localDataSource.getById(id) +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt index 5833ac9e3..054bdb337 100755 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/di/ViewModelModule.kt @@ -1,6 +1,7 @@ package com.shifthackz.aisdv1.presentation.di import com.shifthackz.aisdv1.presentation.activity.AiStableDiffusionViewModel +import com.shifthackz.aisdv1.presentation.modal.download.DownloadDialogViewModel import com.shifthackz.aisdv1.presentation.modal.embedding.EmbeddingViewModel import com.shifthackz.aisdv1.presentation.modal.extras.ExtrasViewModel import com.shifthackz.aisdv1.presentation.modal.history.InputHistoryViewModel @@ -53,6 +54,7 @@ val viewModelModule = module { viewModelOf(::DonateViewModel) viewModelOf(::BackgroundWorkViewModel) viewModelOf(::LoggerViewModel) + viewModelOf(::DownloadDialogViewModel) viewModel { parameters -> OnBoardingViewModel( diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt index eaabe5d90..a25ea9bbd 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt @@ -23,6 +23,7 @@ import com.shifthackz.aisdv1.presentation.core.GenerationFormUpdateEvent import com.shifthackz.aisdv1.presentation.core.GenerationMviIntent import com.shifthackz.aisdv1.presentation.core.ImageToImageIntent import com.shifthackz.aisdv1.presentation.modal.crop.CropImageModal +import com.shifthackz.aisdv1.presentation.modal.download.DownloadDialog import com.shifthackz.aisdv1.presentation.modal.embedding.EmbeddingScreen import com.shifthackz.aisdv1.presentation.modal.extras.ExtrasScreen import com.shifthackz.aisdv1.presentation.modal.grid.GridBottomSheet @@ -367,5 +368,14 @@ fun ModalRenderer( } ) } + + is Modal.SelectDownloadSource -> DownloadDialog( + modelId = screenModal.modelId, + onDismissRequest = dismiss, + onDownloadSourceSelected = { url -> + processIntent(ServerSetupIntent.LocalModel.DownloadConfirm(screenModal.modelId, url)) + dismiss() + } + ) } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialog.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialog.kt new file mode 100644 index 000000000..31477a1fd --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialog.kt @@ -0,0 +1,215 @@ +package com.shifthackz.aisdv1.presentation.modal.download + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Link +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.shifthackz.aisdv1.presentation.R +import com.shifthackz.android.core.mvi.MviComponent +import org.koin.androidx.compose.koinViewModel +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +private const val GITHUB_WEB_RESOURCE = "github.com" +private const val SDAI_WEB_RESOURCE = "share.moroz.cc" + +@Composable +fun DownloadDialog( + modifier: Modifier = Modifier, + modelId: String, + onDismissRequest: () -> Unit, + onDownloadSourceSelected: (url: String) -> Unit, +) { + MviComponent( + viewModel = koinViewModel().apply { + processIntent(DownloadDialogIntent.LoadModelData(modelId)) + }, + processEffect = { effect -> + when (effect) { + DownloadDialogEffect.Close -> onDismissRequest() + is DownloadDialogEffect.StartDownload -> onDownloadSourceSelected(effect.url) + } + } + ) { state, processIntent -> + ScreenContent( + modifier = modifier, + state = state, + processIntent = processIntent, + ) + } +} + +@Composable +@Preview +private fun ScreenContent( + modifier: Modifier = Modifier, + state: DownloadDialogState = DownloadDialogState(), + processIntent: (DownloadDialogIntent) -> Unit = {}, +) { + Dialog( + onDismissRequest = {}, + properties = DialogProperties( + dismissOnClickOutside = false, + dismissOnBackPress = false, + ), + ) { + Surface( + modifier = modifier.fillMaxHeight(0.38f), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.background, + ) { + Scaffold( + topBar = { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.width(40.dp)) + Text( + text = stringResource(LocalizationR.string.title_select_download_source), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = { processIntent(DownloadDialogIntent.Close) }, + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + ) + } + } + }, + bottomBar = { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Button( + modifier = Modifier + .padding(bottom = 12.dp) + .fillMaxWidth(0.65f), + onClick = { processIntent(DownloadDialogIntent.StartDownload) }, + ) { + Icon( + modifier = Modifier.padding(end = 8.dp), + imageVector = Icons.Default.Download, + contentDescription = null, + ) + Text( + text = stringResource(LocalizationR.string.download), + color = LocalContentColor.current, + ) + } + } + }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues), + ) { + items( + count = state.sources.size, + key = { index -> state.sources[index] } + ) { index -> + val (url, selected) = state.sources[index] + Row( + modifier = Modifier + .padding(vertical = 8.dp, horizontal = 16.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(color = MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.8f)) + .defaultMinSize(minHeight = 50.dp) + .border( + width = 2.dp, + shape = RoundedCornerShape(16.dp), + color = if (selected) { + MaterialTheme.colorScheme.primary + } else { + Color.Transparent + }, + ) + .clickable { processIntent(DownloadDialogIntent.SelectSource(url)) }, + verticalAlignment = Alignment.CenterVertically, + ) { + val webResource = remember { + runCatching { + url.split("://")[1].split("/").first() + }.getOrElse { "" } + } + + val iconModifier = Modifier + .size(42.dp) + .padding(horizontal = 8.dp) + + when (webResource) { + GITHUB_WEB_RESOURCE -> Icon( + modifier = iconModifier, + painter = painterResource(R.drawable.ic_github), + contentDescription = null, + ) + + SDAI_WEB_RESOURCE -> Image( + modifier = iconModifier, + painter = painterResource(R.drawable.ic_sdai_logo), + contentDescription = null, + ) + + else -> Icon( + modifier = iconModifier, + imageVector = Icons.Default.Link, + contentDescription = null, + ) + } + + Text( + text = webResource, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogEffect.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogEffect.kt new file mode 100644 index 000000000..d672f7832 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogEffect.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.presentation.modal.download + +import com.shifthackz.android.core.mvi.MviEffect + +sealed interface DownloadDialogEffect : MviEffect { + + data object Close : DownloadDialogEffect + + data class StartDownload(val url: String) : DownloadDialogEffect +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogIntent.kt new file mode 100644 index 000000000..6940e12b5 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogIntent.kt @@ -0,0 +1,14 @@ +package com.shifthackz.aisdv1.presentation.modal.download + +import com.shifthackz.android.core.mvi.MviIntent + +sealed interface DownloadDialogIntent : MviIntent { + + data class LoadModelData(val id: String): DownloadDialogIntent + + data class SelectSource(val url: String) : DownloadDialogIntent + + data object Close : DownloadDialogIntent + + data object StartDownload : DownloadDialogIntent +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogState.kt new file mode 100644 index 000000000..70d3788ad --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogState.kt @@ -0,0 +1,13 @@ +package com.shifthackz.aisdv1.presentation.modal.download + +import androidx.compose.runtime.Immutable +import com.shifthackz.android.core.mvi.MviState + +@Immutable +data class DownloadDialogState( + val sources: List> = emptyList(), +) : MviState { + + val selectedUrl: String + get() = sources.find { (_, selected) -> selected }?.first ?: "" +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogViewModel.kt new file mode 100644 index 000000000..2d648714c --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/download/DownloadDialogViewModel.kt @@ -0,0 +1,42 @@ +package com.shifthackz.aisdv1.presentation.modal.download + +import com.shifthackz.aisdv1.core.common.log.errorLog +import com.shifthackz.aisdv1.core.common.schedulers.DispatchersProvider +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersProvider +import com.shifthackz.aisdv1.core.common.schedulers.subscribeOnMainThread +import com.shifthackz.aisdv1.core.viewmodel.MviRxViewModel +import com.shifthackz.aisdv1.domain.usecase.downloadable.GetLocalModelUseCase +import io.reactivex.rxjava3.kotlin.subscribeBy + +class DownloadDialogViewModel( + private val getLocalModelUseCase: GetLocalModelUseCase, + private val schedulersProvider: SchedulersProvider, + dispatchersProvider: DispatchersProvider, +) : MviRxViewModel() { + + override val initialState = DownloadDialogState() + + override val effectDispatcher = dispatchersProvider.immediate + + override fun processIntent(intent: DownloadDialogIntent) { + when (intent) { + is DownloadDialogIntent.LoadModelData -> !getLocalModelUseCase(intent.id) + .subscribeOnMainThread(schedulersProvider) + .subscribeBy(::errorLog) { model -> + updateState { + it.copy(sources = model.sources.mapIndexed { i, url -> url to (i == 0) }) + } + } + + is DownloadDialogIntent.SelectSource -> updateState { + it.copy(sources = it.sources.map { (url, _) -> url to (url == intent.url) }) + } + + DownloadDialogIntent.StartDownload -> emitEffect( + DownloadDialogEffect.StartDownload(currentState.selectedUrl) + ) + + DownloadDialogIntent.Close -> emitEffect(DownloadDialogEffect.Close) + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt index de4349d80..98983733b 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt @@ -123,4 +123,6 @@ sealed interface Modal { data class LDScheduler(val scheduler: SchedulersToken) : Modal data class GalleryGrid(val grid: Grid) : Modal + + data class SelectDownloadSource(val modelId: String): Modal } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupIntent.kt index 425f9ef65..9735a3583 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupIntent.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupIntent.kt @@ -95,10 +95,10 @@ sealed interface ServerSetupIntent : MviIntent { sealed interface LocalModel : ServerSetupIntent { - val model: ServerSetupState.LocalModel + data class ClickReduce(val model: ServerSetupState.LocalModel) : LocalModel - data class ClickReduce(override val model: ServerSetupState.LocalModel) : LocalModel + data class DownloadConfirm(val modelId: String, val url: String): LocalModel - data class DeleteConfirm(override val model: ServerSetupState.LocalModel) : LocalModel + data class DeleteConfirm(val model: ServerSetupState.LocalModel) : LocalModel } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModel.kt index d054a301d..0e5601955 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModel.kt @@ -226,6 +226,10 @@ class ServerSetupViewModel( is ServerSetupIntent.SelectLocalModelPath -> updateState { state -> state.withLocalCustomModelPath(intent.value) } + + is ServerSetupIntent.LocalModel.DownloadConfirm -> with(intent) { + download(modelId, url) + } } private fun validateAndConnectToServer() { @@ -296,6 +300,7 @@ class ServerSetupViewModel( } validation.isValid } + else -> { currentState.localMediaPipeModels.find { it.selected && it.downloaded } != null } @@ -441,46 +446,51 @@ class ServerSetupViewModel( it.copy(screenModal = Modal.DeleteLocalModelConfirm(localModel())) } // User requested new download operation - else -> { - updateState { state -> - state.withUpdatedLocalModel( - localModel().copy(downloadState = DownloadState.Downloading()), - ) - } - !downloadModelUseCase(localModel().id) - .distinctUntilChanged() - .doOnSubscribe { wakeLockInterActor.acquireWakelockUseCase() } - .doFinally { wakeLockInterActor.releaseWakeLockUseCase() } - .subscribeOnMainThread(schedulersProvider) - .subscribeBy( - onError = { t -> - errorLog(t) - val message = t.localizedMessage ?: "Error" - updateState { state -> - state.withUpdatedLocalModel( - localModel().copy( - downloadState = DownloadState.Error(t), - ), - ) - } - setScreenModal(Modal.Error(message.asUiText())) - }, - onNext = { downloadState -> - updateState { state -> - state.withUpdatedLocalModel( - localModel().copy( - downloadState = downloadState, - downloaded = downloadState is DownloadState.Complete - ), - ) - } - }, - ) - .also { downloadDisposables.add(localModel().id to it) } - } + else -> setScreenModal(Modal.SelectDownloadSource(localModel().id)) } } + private fun download(modelId: String, url: String) { + val localModel = + currentState.localModels.firstOrNull { it.id == modelId } ?: return + + updateState { state -> + state.withUpdatedLocalModel( + localModel.copy(downloadState = DownloadState.Downloading()), + ) + } + !downloadModelUseCase(localModel.id, url) + .distinctUntilChanged() + .doOnSubscribe { wakeLockInterActor.acquireWakelockUseCase() } + .doFinally { wakeLockInterActor.releaseWakeLockUseCase() } + .subscribeOnMainThread(schedulersProvider) + .subscribeBy( + onError = { t -> + errorLog(t) + val message = t.localizedMessage ?: "Error" + updateState { state -> + state.withUpdatedLocalModel( + localModel.copy( + downloadState = DownloadState.Error(t), + ), + ) + } + setScreenModal(Modal.Error(message.asUiText())) + }, + onNext = { downloadState -> + updateState { state -> + state.withUpdatedLocalModel( + localModel.copy( + downloadState = downloadState, + downloaded = downloadState is DownloadState.Complete + ), + ) + } + }, + ) + .also { downloadDisposables.add(localModel.id to it) } + } + private fun setScreenModal(value: Modal) = updateState { it.copy(screenModal = value) } diff --git a/presentation/src/main/res/drawable/ic_github.xml b/presentation/src/main/res/drawable/ic_github.xml new file mode 100644 index 000000000..3a1abe804 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_github.xml @@ -0,0 +1,10 @@ + + + From f9bb7e11847c15bb9abe095d01a2d5fa0b2ff12a Mon Sep 17 00:00:00 2001 From: ShiftHackZ Date: Sat, 5 Apr 2025 23:40:42 +0300 Subject: [PATCH 2/3] Update versions --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9b510532..184c54499 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -versionName = "0.6.7" -versionCode = "188" +versionName = "0.6.8" +versionCode = "190" targetSdk = "34" compileSdk = "35" minSdk = "24" From 102793bde0ed3bfedab868b7b6d78df6be9e7af6 Mon Sep 17 00:00:00 2001 From: ShiftHackZ Date: Sat, 5 Apr 2025 23:58:40 +0300 Subject: [PATCH 3/3] Fix Unit tests --- .../DownloadableModelRepositoryImplTest.kt | 21 +++---------------- .../DownloadModelUseCaseImplTest.kt | 12 +++++------ .../screen/setup/ServerSetupViewModelTest.kt | 4 ++-- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/data/src/test/java/com/shifthackz/aisdv1/data/repository/DownloadableModelRepositoryImplTest.kt b/data/src/test/java/com/shifthackz/aisdv1/data/repository/DownloadableModelRepositoryImplTest.kt index 379bc3c59..b246a019f 100644 --- a/data/src/test/java/com/shifthackz/aisdv1/data/repository/DownloadableModelRepositoryImplTest.kt +++ b/data/src/test/java/com/shifthackz/aisdv1/data/repository/DownloadableModelRepositoryImplTest.kt @@ -219,21 +219,6 @@ class DownloadableModelRepositoryImplTest { .assertNotComplete() } - @Test - fun `given attempt to download model, local data source has no such model, expected error value`() { - every { - stubLocalDataSource.getById(any()) - } returns Single.error(stubException) - - repository - .download("5598") - .test() - .assertNoValues() - .assertError(stubException) - .await() - .assertNotComplete() - } - @Test fun `given attempt to download model, local data source has such model, download succeeds, expected unknown, downloading, complete values`() { every { @@ -241,7 +226,7 @@ class DownloadableModelRepositoryImplTest { } returns Single.just(mockLocalAiModel) val stubObserver = repository - .download("5598") + .download("5598", "https://moroz.cc/stub.zip") .test() stubDownloadState.onNext(DownloadState.Unknown) @@ -276,7 +261,7 @@ class DownloadableModelRepositoryImplTest { } returns Single.just(mockLocalAiModel) val stubObserver = repository - .download("5598") + .download("5598", "https://moroz.cc/stub.zip") .test() stubDownloadState.onNext(DownloadState.Unknown) @@ -309,7 +294,7 @@ class DownloadableModelRepositoryImplTest { } returns Observable.error(stubException) repository - .download("5598") + .download("5598", "https://moroz.cc/stub.zip") .test() .assertError(stubException) .assertNoValues() diff --git a/domain/src/test/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCaseImplTest.kt b/domain/src/test/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCaseImplTest.kt index 669200f4a..f5c463a75 100644 --- a/domain/src/test/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCaseImplTest.kt +++ b/domain/src/test/java/com/shifthackz/aisdv1/domain/usecase/downloadable/DownloadModelUseCaseImplTest.kt @@ -23,13 +23,13 @@ class DownloadModelUseCaseImplTest { @Before fun initialize() { - whenever(stubRepository.download(any())) + whenever(stubRepository.download(any(), any())) .thenReturn(stubDownloadStatus) } @Test fun `given download running, then finishes successfully, expected final state is Complete`() { - val stubObserver = useCase("5598").test() + val stubObserver = useCase("5598", "https://moroz.cc/stub.zip").test() stubDownloadStatus.onNext(DownloadState.Unknown) @@ -58,7 +58,7 @@ class DownloadModelUseCaseImplTest { @Test fun `given download running, then fails, expected final state is Error`() { - val stubObserver = useCase("5598").test() + val stubObserver = useCase("5598", "https://moroz.cc/stub.zip").test() stubDownloadStatus.onNext(DownloadState.Unknown) @@ -87,7 +87,7 @@ class DownloadModelUseCaseImplTest { @Test fun `given download running, then fails, then user restarts download, then completes, expected state Error on 1st try, final state is Complete`() { - val stubObserver = useCase("5598").test() + val stubObserver = useCase("5598", "https://moroz.cc/stub.zip").test() stubDownloadStatus.onNext(DownloadState.Unknown) @@ -140,10 +140,10 @@ class DownloadModelUseCaseImplTest { @Test fun `given observable terminated with unexpected error, expected error value`() { - whenever(stubRepository.download(any())) + whenever(stubRepository.download(any(), any())) .thenReturn(Observable.error(stubTerminateException)) - useCase("5598") + useCase("5598", "https://moroz.cc/stub.zip") .test() .assertError(stubTerminateException) .await() diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModelTest.kt index c9be862b4..8713466db 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModelTest.kt @@ -148,7 +148,7 @@ class ServerSetupViewModelTest : CoreViewModelTest() { @Test fun `given received LocalModel ClickReduce intent, model not downloaded, expected UI state is Downloading, wakeLocks called`() { every { - stubDownloadModelUseCase(any()) + stubDownloadModelUseCase(any(), any()) } returns Observable.just(DownloadState.Downloading(22)) every { @@ -180,7 +180,7 @@ class ServerSetupViewModelTest : CoreViewModelTest() { stubWakeLockInterActor.releaseWakeLockUseCase() } verify { - stubDownloadModelUseCase("1") + stubDownloadModelUseCase("1", "https://example.com/1.html") } }