diff --git a/core/localization/src/main/res/values/strings.xml b/core/localization/src/main/res/values/strings.xml index 8263cc8f..1ef6a8f3 100755 --- a/core/localization/src/main/res/values/strings.xml +++ b/core/localization/src/main/res/values/strings.xml @@ -170,6 +170,7 @@ Original Info + Backup Text to Image Image to Image Gallery @@ -401,4 +402,14 @@ Intellectual property infringement Adult content Other + + Select operation + Proceed + Restore + Create + Create a new backup + Restore from the backup + Generation provider configuration + Appication settings + Gallery diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/di/RepositoryModule.kt b/data/src/main/java/com/shifthackz/aisdv1/data/di/RepositoryModule.kt index 1358c8a9..cd4e0f7c 100755 --- a/data/src/main/java/com/shifthackz/aisdv1/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/shifthackz/aisdv1/data/di/RepositoryModule.kt @@ -2,6 +2,7 @@ package com.shifthackz.aisdv1.data.di import android.content.Context import android.os.PowerManager +import com.shifthackz.aisdv1.data.repository.BackupRepositoryImpl import com.shifthackz.aisdv1.data.repository.DownloadableModelRepositoryImpl import com.shifthackz.aisdv1.data.repository.EmbeddingsRepositoryImpl import com.shifthackz.aisdv1.data.repository.GenerationResultRepositoryImpl @@ -27,6 +28,7 @@ import com.shifthackz.aisdv1.data.repository.SwarmUiGenerationRepositoryImpl import com.shifthackz.aisdv1.data.repository.SwarmUiModelsRepositoryImpl import com.shifthackz.aisdv1.data.repository.TemporaryGenerationResultRepositoryImpl import com.shifthackz.aisdv1.data.repository.WakeLockRepositoryImpl +import com.shifthackz.aisdv1.domain.repository.BackupRepository import com.shifthackz.aisdv1.domain.repository.DownloadableModelRepository import com.shifthackz.aisdv1.domain.repository.EmbeddingsRepository import com.shifthackz.aisdv1.domain.repository.GenerationResultRepository @@ -66,6 +68,7 @@ val repositoryModule = module { } singleOf(::TemporaryGenerationResultRepositoryImpl) bind TemporaryGenerationResultRepository::class + factoryOf(::BackupRepositoryImpl) bind BackupRepository::class factoryOf(::LocalDiffusionGenerationRepositoryImpl) bind LocalDiffusionGenerationRepository::class factoryOf(::MediaPipeGenerationRepositoryImpl) bind MediaPipeGenerationRepository::class factoryOf(::HordeGenerationRepositoryImpl) bind HordeGenerationRepository::class diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/local/GenerationResultLocalDataSource.kt b/data/src/main/java/com/shifthackz/aisdv1/data/local/GenerationResultLocalDataSource.kt index 90c7bd57..a875f12d 100644 --- a/data/src/main/java/com/shifthackz/aisdv1/data/local/GenerationResultLocalDataSource.kt +++ b/data/src/main/java/com/shifthackz/aisdv1/data/local/GenerationResultLocalDataSource.kt @@ -6,6 +6,7 @@ import com.shifthackz.aisdv1.domain.datasource.GenerationResultDataSource import com.shifthackz.aisdv1.domain.entity.AiGenerationResult import com.shifthackz.aisdv1.storage.db.persistent.dao.GenerationResultDao import com.shifthackz.aisdv1.storage.db.persistent.entity.GenerationResultEntity +import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single internal class GenerationResultLocalDataSource( @@ -16,6 +17,10 @@ internal class GenerationResultLocalDataSource( .mapDomainToEntity() .let(dao::insert) + override fun insert(results: List) = results + .mapDomainToEntity() + .let(dao::insert) + override fun queryAll(): Single> = dao .query() .map(List::mapEntityToDomain) diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/repository/BackupRepositoryImpl.kt b/data/src/main/java/com/shifthackz/aisdv1/data/repository/BackupRepositoryImpl.kt new file mode 100644 index 00000000..6b7b1cc3 --- /dev/null +++ b/data/src/main/java/com/shifthackz/aisdv1/data/repository/BackupRepositoryImpl.kt @@ -0,0 +1,228 @@ +package com.shifthackz.aisdv1.data.repository + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.shifthackz.aisdv1.core.common.appbuild.BuildInfoProvider +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersToken +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_AI_AUTO_SAVE +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_ALLOW_LOCAL_DIFFUSION_CANCEL +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_BACKGROUND_GENERATION +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_DEMO_MODE +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_DESIGN_COLOR_TOKEN +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_DESIGN_DARK_THEME +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_DESIGN_DARK_TOKEN +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_DESIGN_DYNAMIC_COLORS +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_DESIGN_SYSTEM_DARK_THEME +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_DEVELOPER_MODE +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_FORCE_SETUP_AFTER_UPDATE +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_FORM_ALWAYS_SHOW_ADVANCED_OPTIONS +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_FORM_PROMPT_TAGGED_INPUT +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_GALLERY_GRID +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_HORDE_API_KEY +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_HUGGING_FACE_API_KEY +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_HUGGING_FACE_MODEL_KEY +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_LOCAL_DIFFUSION_CUSTOM_MODEL_PATH +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_LOCAL_DIFFUSION_SCHEDULER_THREAD +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_LOCAL_MODEL_ID +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_LOCAL_NN_API +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_MONITOR_CONNECTIVITY +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_ON_BOARDING_COMPLETE +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_OPEN_AI_API_KEY +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_SAVE_TO_MEDIA_STORE +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_SD_MODEL +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_SERVER_SOURCE +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_SERVER_URL +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_STABILITY_AI_API_KEY +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_STABILITY_AI_ENGINE_ID_KEY +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_SWARM_MODEL +import com.shifthackz.aisdv1.data.preference.PreferenceManagerImpl.Companion.KEY_SWARM_SERVER_URL +import com.shifthackz.aisdv1.domain.datasource.GenerationResultDataSource +import com.shifthackz.aisdv1.domain.entity.Backup +import com.shifthackz.aisdv1.domain.entity.BackupEntryToken +import com.shifthackz.aisdv1.domain.entity.Grid +import com.shifthackz.aisdv1.domain.entity.ServerSource +import com.shifthackz.aisdv1.domain.preference.PreferenceManager +import com.shifthackz.aisdv1.domain.repository.BackupRepository +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +import java.util.Date + +internal class BackupRepositoryImpl( + private val gson: Gson, + private val generationLds: GenerationResultDataSource.Local, + private val preferenceManager: PreferenceManager, + private val buildInfoProvider: BuildInfoProvider, +) : BackupRepository { + + override fun create(tokens: List>): Single { + val chainGallery = if (tokens.contains(BackupEntryToken.Gallery to true)) { + generationLds.queryAll() + } else { + Single.just(emptyList()) + } + val chainAppConfig = if (tokens.contains(BackupEntryToken.AppConfiguration to true)) { + val map = with(preferenceManager) { + mapOf( + KEY_SERVER_URL to automatic1111ServerUrl, + KEY_SWARM_SERVER_URL to swarmUiServerUrl, + KEY_SWARM_MODEL to swarmUiModel, + KEY_DEMO_MODE to demoMode, + KEY_DEVELOPER_MODE to developerMode, + KEY_LOCAL_DIFFUSION_CUSTOM_MODEL_PATH to localOnnxCustomModelPath, + KEY_ALLOW_LOCAL_DIFFUSION_CANCEL to localOnnxAllowCancel, + KEY_LOCAL_DIFFUSION_SCHEDULER_THREAD to localOnnxSchedulerThread, + KEY_MONITOR_CONNECTIVITY to monitorConnectivity, + KEY_AI_AUTO_SAVE to autoSaveAiResults, + KEY_SAVE_TO_MEDIA_STORE to saveToMediaStore, + KEY_FORM_ALWAYS_SHOW_ADVANCED_OPTIONS to formAdvancedOptionsAlwaysShow, + KEY_FORM_PROMPT_TAGGED_INPUT to formPromptTaggedInput, + KEY_SERVER_SOURCE to source, + KEY_SD_MODEL to sdModel, + KEY_HORDE_API_KEY to hordeApiKey, + KEY_OPEN_AI_API_KEY to openAiApiKey, + KEY_HUGGING_FACE_API_KEY to huggingFaceApiKey, + KEY_HUGGING_FACE_MODEL_KEY to huggingFaceModel, + KEY_STABILITY_AI_API_KEY to stabilityAiApiKey, + KEY_STABILITY_AI_ENGINE_ID_KEY to stabilityAiEngineId, + KEY_ON_BOARDING_COMPLETE to onBoardingComplete, + KEY_FORCE_SETUP_AFTER_UPDATE to forceSetupAfterUpdate, + KEY_LOCAL_MODEL_ID to localOnnxModelId, + KEY_LOCAL_NN_API to localOnnxUseNNAPI, + KEY_DESIGN_DYNAMIC_COLORS to designUseSystemColorPalette, + KEY_DESIGN_SYSTEM_DARK_THEME to designUseSystemDarkTheme, + KEY_DESIGN_DARK_THEME to designDarkTheme, + KEY_DESIGN_COLOR_TOKEN to designColorToken, + KEY_DESIGN_DARK_TOKEN to designDarkThemeToken, + KEY_BACKGROUND_GENERATION to backgroundGeneration, + KEY_GALLERY_GRID to galleryGrid, + ) + } + Single.just(map) + } else { + Single.just(emptyMap()) + } + return Single + .zip(chainGallery, chainAppConfig, ::Pair) + .map { (gallery, config) -> + Backup( + generatedAt = Date(), + appVersion = buildInfoProvider.toString(), + appConfiguration = config, + gallery = gallery, + ) + } + .map { backup -> gson.toJson(backup).encodeToByteArray() } + } + + override fun restore(bytes: ByteArray): Completable = Single + .just(bytes) + .map { it.decodeToString() } + .map { gson.fromJson(it, object : TypeToken() {}.type) } + .flatMapCompletable { backup -> + Completable.mergeArray( + Completable.fromAction { + backup.appConfiguration.entries.forEach { (key, value) -> + when (key) { + KEY_SERVER_URL -> { + preferenceManager.automatic1111ServerUrl = value as String + } + KEY_SWARM_SERVER_URL -> { + preferenceManager.swarmUiServerUrl = value as String + } + KEY_SWARM_MODEL -> { + preferenceManager.swarmUiModel = value as String + } + KEY_DEMO_MODE -> { + preferenceManager.demoMode = value as Boolean + } + KEY_DEVELOPER_MODE -> { + preferenceManager.developerMode = value as Boolean + } + KEY_LOCAL_DIFFUSION_CUSTOM_MODEL_PATH -> { + preferenceManager.localOnnxCustomModelPath = value as String + } + KEY_ALLOW_LOCAL_DIFFUSION_CANCEL -> { + preferenceManager.localOnnxAllowCancel = value as Boolean + } + KEY_LOCAL_DIFFUSION_SCHEDULER_THREAD -> { + preferenceManager.localOnnxSchedulerThread = SchedulersToken.valueOf(value as String) + } + KEY_MONITOR_CONNECTIVITY -> { + preferenceManager.monitorConnectivity = value as Boolean + } + KEY_AI_AUTO_SAVE -> { + preferenceManager.autoSaveAiResults = value as Boolean + } + KEY_SAVE_TO_MEDIA_STORE -> { + preferenceManager.saveToMediaStore = value as Boolean + } + KEY_FORM_ALWAYS_SHOW_ADVANCED_OPTIONS -> { + preferenceManager.formAdvancedOptionsAlwaysShow = value as Boolean + } + KEY_FORM_PROMPT_TAGGED_INPUT -> { + preferenceManager.formPromptTaggedInput = value as Boolean + } + KEY_SERVER_SOURCE -> { + preferenceManager.source = ServerSource.valueOf(value as String) + } + KEY_SD_MODEL -> { + preferenceManager.sdModel = value as String + } + KEY_HORDE_API_KEY -> { + preferenceManager.hordeApiKey = value as String + } + KEY_OPEN_AI_API_KEY -> { + preferenceManager.openAiApiKey = value as String + } + KEY_HUGGING_FACE_API_KEY -> { + preferenceManager.huggingFaceApiKey = value as String + } + KEY_HUGGING_FACE_MODEL_KEY -> { + preferenceManager.huggingFaceModel = value as String + } + KEY_STABILITY_AI_API_KEY -> { + preferenceManager.stabilityAiApiKey = value as String + } + KEY_STABILITY_AI_ENGINE_ID_KEY -> { + preferenceManager.stabilityAiEngineId = value as String + } + KEY_ON_BOARDING_COMPLETE -> { + preferenceManager.onBoardingComplete = value as Boolean + } + KEY_FORCE_SETUP_AFTER_UPDATE -> { + preferenceManager.forceSetupAfterUpdate = value as Boolean + } + KEY_LOCAL_MODEL_ID -> { + preferenceManager.localOnnxModelId = value as String + } + KEY_LOCAL_NN_API -> { + preferenceManager.localOnnxUseNNAPI = value as Boolean + } + KEY_DESIGN_DYNAMIC_COLORS -> { + preferenceManager.designUseSystemColorPalette = value as Boolean + } + KEY_DESIGN_SYSTEM_DARK_THEME -> { + preferenceManager.designUseSystemDarkTheme = value as Boolean + } + KEY_DESIGN_DARK_THEME -> { + preferenceManager.designDarkTheme = value as Boolean + } + KEY_DESIGN_COLOR_TOKEN -> { + preferenceManager.designColorToken = value as String + } + KEY_DESIGN_DARK_TOKEN -> { + preferenceManager.designDarkThemeToken = value as String + } + KEY_BACKGROUND_GENERATION -> { + preferenceManager.backgroundGeneration = value as Boolean + } + KEY_GALLERY_GRID -> { + preferenceManager.galleryGrid = Grid.valueOf(value as String) + } + } + } + }, + generationLds.insert(backup.gallery), + ) + } +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/datasource/GenerationResultDataSource.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/datasource/GenerationResultDataSource.kt index 8b28ef90..62c83a0d 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/datasource/GenerationResultDataSource.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/datasource/GenerationResultDataSource.kt @@ -8,6 +8,7 @@ sealed interface GenerationResultDataSource { interface Local : GenerationResultDataSource { fun insert(result: AiGenerationResult): Single + fun insert(results: List): Completable fun queryAll(): Single> fun queryPage(limit: Int, offset: Int): Single> fun queryById(id: Long): Single 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 0dda2ed4..dd5ac7c5 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 @@ -4,6 +4,10 @@ import com.shifthackz.aisdv1.domain.interactor.settings.SetupConnectionInterActo import com.shifthackz.aisdv1.domain.interactor.settings.SetupConnectionInterActorImpl import com.shifthackz.aisdv1.domain.interactor.wakelock.WakeLockInterActor import com.shifthackz.aisdv1.domain.interactor.wakelock.WakeLockInterActorImpl +import com.shifthackz.aisdv1.domain.usecase.backup.CreateBackupUseCase +import com.shifthackz.aisdv1.domain.usecase.backup.CreateBackupUseCaseImpl +import com.shifthackz.aisdv1.domain.usecase.backup.RestoreBackupUseCase +import com.shifthackz.aisdv1.domain.usecase.backup.RestoreBackupUseCaseImpl import com.shifthackz.aisdv1.domain.usecase.caching.ClearAppCacheUseCase import com.shifthackz.aisdv1.domain.usecase.caching.ClearAppCacheUseCaseImpl import com.shifthackz.aisdv1.domain.usecase.caching.DataPreLoaderUseCase @@ -127,6 +131,8 @@ import org.koin.dsl.bind import org.koin.dsl.module internal val useCasesModule = module { + factoryOf(::CreateBackupUseCaseImpl) bind CreateBackupUseCase::class + factoryOf(::RestoreBackupUseCaseImpl) bind RestoreBackupUseCase::class factoryOf(::TextToImageUseCaseImpl) bind TextToImageUseCase::class factoryOf(::ImageToImageUseCaseImpl) bind ImageToImageUseCase::class factoryOf(::PingStableDiffusionServiceUseCaseImpl) bind PingStableDiffusionServiceUseCase::class diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/Backup.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/Backup.kt new file mode 100644 index 00000000..7e4a5371 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/Backup.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.domain.entity + +import java.util.Date + +data class Backup( + val generatedAt: Date = Date(), + val appVersion: String = "", + val appConfiguration: Map = mapOf(), + val gallery: List = emptyList(), +) diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/BackupEntryToken.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/BackupEntryToken.kt new file mode 100644 index 00000000..cfd0529e --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/BackupEntryToken.kt @@ -0,0 +1,7 @@ +package com.shifthackz.aisdv1.domain.entity + +enum class BackupEntryToken { + AppConfiguration, +// AppPreferences, + Gallery; +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/BackupRepository.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/BackupRepository.kt new file mode 100644 index 00000000..c2c6a14c --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/BackupRepository.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.domain.repository + +import com.shifthackz.aisdv1.domain.entity.BackupEntryToken +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single + +interface BackupRepository { + fun create(tokens: List>): Single + fun restore(bytes: ByteArray): Completable +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/CreateBackupUseCase.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/CreateBackupUseCase.kt new file mode 100644 index 00000000..60101411 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/CreateBackupUseCase.kt @@ -0,0 +1,8 @@ +package com.shifthackz.aisdv1.domain.usecase.backup + +import com.shifthackz.aisdv1.domain.entity.BackupEntryToken +import io.reactivex.rxjava3.core.Single + +interface CreateBackupUseCase { + operator fun invoke(tokens: List>): Single +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/CreateBackupUseCaseImpl.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/CreateBackupUseCaseImpl.kt new file mode 100644 index 00000000..43bd1156 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/CreateBackupUseCaseImpl.kt @@ -0,0 +1,12 @@ +package com.shifthackz.aisdv1.domain.usecase.backup + +import com.shifthackz.aisdv1.domain.entity.BackupEntryToken +import com.shifthackz.aisdv1.domain.repository.BackupRepository + +internal class CreateBackupUseCaseImpl( + private val backupRepository: BackupRepository, +) : CreateBackupUseCase { + + override fun invoke(tokens: List>) = backupRepository + .create(tokens) +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/RestoreBackupUseCase.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/RestoreBackupUseCase.kt new file mode 100644 index 00000000..d32a0478 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/RestoreBackupUseCase.kt @@ -0,0 +1,7 @@ +package com.shifthackz.aisdv1.domain.usecase.backup + +import io.reactivex.rxjava3.core.Completable + +interface RestoreBackupUseCase { + operator fun invoke(data: ByteArray): Completable +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/RestoreBackupUseCaseImpl.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/RestoreBackupUseCaseImpl.kt new file mode 100644 index 00000000..cd8fbe50 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/backup/RestoreBackupUseCaseImpl.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.domain.usecase.backup + +import com.shifthackz.aisdv1.domain.repository.BackupRepository + +internal class RestoreBackupUseCaseImpl( + private val backupRepository: BackupRepository, +) : RestoreBackupUseCase { + + override fun invoke(data: ByteArray) = backupRepository.restore(data) +} 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 5833ac9e..0c4f7278 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 @@ -6,6 +6,7 @@ import com.shifthackz.aisdv1.presentation.modal.extras.ExtrasViewModel import com.shifthackz.aisdv1.presentation.modal.history.InputHistoryViewModel import com.shifthackz.aisdv1.presentation.modal.tag.EditTagViewModel import com.shifthackz.aisdv1.presentation.model.LaunchSource +import com.shifthackz.aisdv1.presentation.screen.backup.BackupViewModel import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuViewModel import com.shifthackz.aisdv1.presentation.screen.donate.DonateViewModel import com.shifthackz.aisdv1.presentation.screen.drawer.DrawerViewModel @@ -53,6 +54,7 @@ val viewModelModule = module { viewModelOf(::DonateViewModel) viewModelOf(::BackgroundWorkViewModel) viewModelOf(::LoggerViewModel) + viewModelOf(::BackupViewModel) 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 eaabe5d9..e09a9973 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 @@ -31,6 +31,7 @@ import com.shifthackz.aisdv1.presentation.modal.language.LanguageBottomSheet import com.shifthackz.aisdv1.presentation.modal.ldscheduler.LDSchedulerBottomSheet import com.shifthackz.aisdv1.presentation.modal.tag.EditTagDialog import com.shifthackz.aisdv1.presentation.model.Modal +import com.shifthackz.aisdv1.presentation.screen.backup.BackupIntent import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuIntent import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailIntent import com.shifthackz.aisdv1.presentation.screen.gallery.list.GalleryIntent @@ -63,6 +64,7 @@ fun ModalRenderer( processIntent(InPaintIntent.ScreenModal.Dismiss) processIntent(DebugMenuIntent.DismissModal) processIntent(ReportIntent.DismissError) + processIntent(BackupIntent.DismissModal) } val context = LocalContext.current when (screenModal) { diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/NavigationRoute.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/NavigationRoute.kt index d898633b..f0c88e1a 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/NavigationRoute.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/NavigationRoute.kt @@ -28,6 +28,9 @@ sealed interface NavigationRoute { @Serializable data object Donate : NavigationRoute + @Serializable + data object Backup : NavigationRoute + @Serializable data class Onboarding(val source: LaunchSource) : NavigationRoute diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/DrawerNavGraph.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/DrawerNavGraph.kt index a5b0463d..60db5183 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/DrawerNavGraph.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/DrawerNavGraph.kt @@ -1,6 +1,7 @@ package com.shifthackz.aisdv1.presentation.navigation.graph import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Backup import androidx.compose.material.icons.filled.DeveloperMode import androidx.compose.material.icons.filled.SettingsEthernet import androidx.compose.material.icons.filled.Web @@ -24,6 +25,7 @@ fun mainDrawerNavItems(settings: Settings? = null): List = buildList { } add(settingsTab()) add(configuration()) + add(backup()) settings?.developerMode?.takeIf { it }?.let { add(developerMode()) } @@ -50,10 +52,18 @@ private fun configuration() = NavItem( ), ) +private fun backup() = NavItem( + name = LocalizationR.string.title_backup.asUiText(), + navRoute = NavigationRoute.Backup, + icon = NavItem.Icon.Vector( + vector = Icons.Default.Backup, + ), +) + private fun developerMode() = NavItem( name = LocalizationR.string.title_debug_menu.asUiText(), navRoute = NavigationRoute.Debug, icon = NavItem.Icon.Vector( vector = Icons.Default.DeveloperMode, - ) + ), ) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/MainNavGraph.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/MainNavGraph.kt index e9de82a6..994a7148 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/MainNavGraph.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/graph/MainNavGraph.kt @@ -1,11 +1,15 @@ package com.shifthackz.aisdv1.presentation.navigation.graph +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.navigation.NavigationRoute +import com.shifthackz.aisdv1.presentation.screen.backup.BackupScreen import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuScreen import com.shifthackz.aisdv1.presentation.screen.donate.DonateScreen import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailScreen @@ -56,7 +60,20 @@ fun NavGraphBuilder.mainNavGraph() { GalleryDetailScreen(itemId = itemId) } - composable { entry -> + composable( + enterTransition = { + slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(500), + ) + }, + exitTransition = { + slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(500), + ) + }, + ) { entry -> val itemId = entry.toRoute().itemId ReportScreen( viewModel = koinViewModel( @@ -85,6 +102,10 @@ fun NavGraphBuilder.mainNavGraph() { DonateScreen() } + composable { + BackupScreen() + } + composable( typeMap = mapOf( typeOf() to NavType.EnumType(LaunchSource::class.java) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt index ed63759d..2425bc92 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt @@ -16,6 +16,8 @@ interface MainRouter : Router { fun navigateToServerSetup(source: LaunchSource) + fun navigateToBackup() + fun navigateToGalleryDetails(itemId: Long) fun navigateToReportImage(itemId: Long) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImpl.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImpl.kt index 2a10039a..86a983bf 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImpl.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImpl.kt @@ -59,6 +59,10 @@ internal class MainRouterImpl : MainRouter { }) } + override fun navigateToBackup() { + effectSubject.onNext(NavigationEffect.Navigate.Route(navRoute = NavigationRoute.Backup)) + } + override fun navigateToGalleryDetails(itemId: Long) { effectSubject.onNext( NavigationEffect.Navigate.Route( diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupEffect.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupEffect.kt new file mode 100644 index 00000000..184807cc --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupEffect.kt @@ -0,0 +1,22 @@ +package com.shifthackz.aisdv1.presentation.screen.backup + +import com.shifthackz.android.core.mvi.MviEffect + +sealed interface BackupEffect : MviEffect { + + data class SaveBackup(val bytes: ByteArray) : BackupEffect { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SaveBackup + + return bytes.contentEquals(other.bytes) + } + + override fun hashCode(): Int { + return bytes.contentHashCode() + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupIntent.kt new file mode 100644 index 00000000..11c15403 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupIntent.kt @@ -0,0 +1,28 @@ +package com.shifthackz.aisdv1.presentation.screen.backup + +import com.shifthackz.aisdv1.domain.entity.BackupEntryToken +import com.shifthackz.android.core.mvi.MviIntent + +interface BackupIntent : MviIntent { + + data object DismissModal : BackupIntent + + data object NavigateBack : BackupIntent + + data object MainButtonClick : BackupIntent + + data class SelectOperation(val value: String) : BackupIntent + + @Suppress("ArrayInDataClass") + data class SelectRestore( + val path: String, + val bytes: ByteArray, + ): BackupIntent + + data class ToggleBackupEntry( + val entry: BackupEntryToken, + val checked: Boolean, + ) : BackupIntent + + enum class OnResult : BackupIntent { Success, Fail } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupScreen.kt new file mode 100644 index 00000000..2136fb47 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupScreen.kt @@ -0,0 +1,267 @@ +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) + +package com.shifthackz.aisdv1.presentation.screen.backup + +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +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.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.core.model.asUiText +import com.shifthackz.aisdv1.presentation.modal.ModalRenderer +import com.shifthackz.aisdv1.presentation.screen.backup.forms.BackupOperationForm +import com.shifthackz.aisdv1.presentation.screen.backup.forms.CreateBackupForm +import com.shifthackz.aisdv1.presentation.screen.backup.forms.RestoreBackupForm +import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupScreenTags +import com.shifthackz.aisdv1.presentation.utils.saveByteArrayToUri +import com.shifthackz.aisdv1.presentation.widget.toolbar.StepBar +import com.shifthackz.android.core.mvi.MviComponent +import org.koin.androidx.compose.koinViewModel +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +@Composable +fun BackupScreen() { + var backup by remember { mutableStateOf(null) } + + val viewModel = koinViewModel() + val context = LocalContext.current + val createDocumentLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/octet-stream"), + onResult = { uri -> + uri ?: run { + viewModel.processIntent(BackupIntent.OnResult.Fail) + return@rememberLauncherForActivityResult + } + backup?.let { + saveByteArrayToUri(context, uri, it) + viewModel.processIntent(BackupIntent.OnResult.Success) + } ?: run { + viewModel.processIntent(BackupIntent.OnResult.Fail) + } + backup = null + } + ) + + MviComponent( + viewModel = viewModel, + processEffect = { effect -> + when (effect) { + is BackupEffect.SaveBackup -> { + backup = effect.bytes + createDocumentLauncher.launch( + "sdai_backup_${System.currentTimeMillis()}.dat", + ) + } + } + }, + ) { state, processIntent -> + BackupScreenContent( + state = state, + processIntent = processIntent, + ) + } +} + +@Composable +private fun BackupScreenContent( + modifier: Modifier = Modifier, + state: BackupState, + processIntent: (BackupIntent) -> Unit = {}, +) { + BackHandler { + processIntent(BackupIntent.NavigateBack) + } + Scaffold( + modifier = modifier, + topBar = { + Column { + CenterAlignedTopAppBar( + title = { + Text( + text = stringResource(id = LocalizationR.string.title_backup), + style = MaterialTheme.typography.headlineMedium, + ) + }, + navigationIcon = { + IconButton( + onClick = { processIntent(BackupIntent.NavigateBack) }, + content = { + Icon( + Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Back button", + ) + }, + ) + }, + ) + if (!state.complete) { + StepBar( + steps = BackupState.Step.entries, + currentStep = state.step, + ) { step -> + when (step) { + BackupState.Step.SelectOperation -> LocalizationR.string.backup_step_operation + BackupState.Step.ProcessBackup -> LocalizationR.string.backup_step_process + }.asUiText() + } + } + } + }, + bottomBar = { + Button( + modifier = Modifier + .navigationBarsPadding() + .height(height = 68.dp) + .background(MaterialTheme.colorScheme.background) + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp, top = 8.dp), + onClick = { processIntent(BackupIntent.MainButtonClick) }, + enabled = !state.loading && when (state.step) { + BackupState.Step.SelectOperation -> state.operation != null + BackupState.Step.ProcessBackup -> when (state.operation) { + is BackupState.Operation.Create -> state.operation.tokens.any { it.second } + is BackupState.Operation.Restore -> state.backupToRestore != null + null -> false + } + }, + ) { + Text( + text = stringResource( + id = if (state.complete) LocalizationR.string.ok + else when (state.step) { + BackupState.Step.ProcessBackup -> when (state.operation) { + is BackupState.Operation.Create -> LocalizationR.string.backup_action_create + is BackupState.Operation.Restore -> LocalizationR.string.backup_action_restore + null -> LocalizationR.string.next + } + + else -> LocalizationR.string.next + }, + ), + color = LocalContentColor.current, + ) + } + }, + ) { paddingValues -> + val pagerState = rememberPagerState( + initialPage = 0, + pageCount = { BackupState.Step.entries.size }, + ) + + AnimatedContent( + modifier = Modifier.padding(paddingValues), + targetState = !state.loading, + label = "backup_state_animator", + ) { contentVisible -> + if (contentVisible) AnimatedContent( + targetState = state.complete, + label = "backup_complete_animator", + ) { completeVisible -> + if (completeVisible) Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + modifier = Modifier.size(100.dp), + imageVector = Icons.Default.CheckCircle, + contentDescription = "Done", + ) + Text( + text = "Success!", + style = MaterialTheme.typography.headlineMedium, + ) + } else HorizontalPager( + state = pagerState, + userScrollEnabled = false, + ) { index -> + when (BackupState.Step.entries[index]) { + BackupState.Step.SelectOperation -> BackupOperationForm( + state = state, + processIntent = processIntent, + ) + + BackupState.Step.ProcessBackup -> when (state.operation) { + is BackupState.Operation.Create -> CreateBackupForm( + state = state, + processIntent = processIntent, + ) + + is BackupState.Operation.Restore -> RestoreBackupForm( + state = state, + processIntent = processIntent, + ) + + else -> Unit + } + } + } + } else Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier + .size(60.dp) + .aspectRatio(1f), + ) + } + } + + LaunchedEffect(state.step) { + pagerState.animateScrollToPage(state.step.ordinal) + } + } + ModalRenderer(state.screenModal) { intent -> + (intent as? BackupIntent)?.let(processIntent::invoke) + } +} + +@Composable +@Preview +private fun PreviewStepOperation() { + BackupScreenContent( + state = BackupState() + ) +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupState.kt new file mode 100644 index 00000000..0a4a4d55 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupState.kt @@ -0,0 +1,31 @@ +package com.shifthackz.aisdv1.presentation.screen.backup + +import androidx.compose.runtime.Immutable +import com.shifthackz.aisdv1.domain.entity.BackupEntryToken +import com.shifthackz.aisdv1.presentation.model.Modal +import com.shifthackz.android.core.mvi.MviState + +@Immutable +data class BackupState( + val screenModal: Modal = Modal.None, + val step: Step = Step.SelectOperation, + val operation: Operation? = null, + val loading: Boolean = false, + val complete: Boolean = false, + val backupToRestore: Pair? = null, +) : MviState { + + enum class Step { + SelectOperation, ProcessBackup; + } + + sealed interface Operation { + data class Create( + val tokens: List> = BackupEntryToken.entries.map { it to false }, + ) : Operation + + data class Restore( + val tokens: List> = BackupEntryToken.entries.map { it to false }, + ) : Operation + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupViewModel.kt new file mode 100644 index 00000000..ec2ffdd0 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/BackupViewModel.kt @@ -0,0 +1,111 @@ +package com.shifthackz.aisdv1.presentation.screen.backup + +import com.shifthackz.aisdv1.core.common.log.errorLog +import com.shifthackz.aisdv1.core.common.schedulers.SchedulersProvider +import com.shifthackz.aisdv1.core.common.schedulers.subscribeOnMainThread +import com.shifthackz.aisdv1.core.model.asUiText +import com.shifthackz.aisdv1.core.viewmodel.MviRxViewModel +import com.shifthackz.aisdv1.domain.usecase.backup.CreateBackupUseCase +import com.shifthackz.aisdv1.domain.usecase.backup.RestoreBackupUseCase +import com.shifthackz.aisdv1.presentation.model.Modal +import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter +import com.shifthackz.android.core.mvi.EmptyEffect +import io.reactivex.rxjava3.kotlin.subscribeBy + +class BackupViewModel( + private val mainRouter: MainRouter, + private val createBackupUseCase: CreateBackupUseCase, + private val restoreBackupUseCase: RestoreBackupUseCase, + private val schedulersProvider: SchedulersProvider, +) : MviRxViewModel() { + + override val initialState = BackupState() + + override fun processIntent(intent: BackupIntent) { + when (intent) { + BackupIntent.NavigateBack -> { + if (currentState.complete || currentState.step == BackupState.Step.SelectOperation) { + mainRouter.navigateBack() + } else { + updateState { + it.copy(step = BackupState.Step.entries.first()) + } + } + } + + BackupIntent.MainButtonClick -> if (currentState.complete) { + mainRouter.navigateBack() + } else when (currentState.step) { + BackupState.Step.SelectOperation -> updateState { + it.copy(step = BackupState.Step.ProcessBackup) + } + + BackupState.Step.ProcessBackup -> when (val op = currentState.operation) { + is BackupState.Operation.Create -> !createBackupUseCase(op.tokens) + .doOnSubscribe { updateState { it.copy(loading = true) } } + .map(BackupEffect::SaveBackup) + .subscribeOnMainThread(schedulersProvider) + .subscribeBy(::errorLog, ::emitEffect) + + is BackupState.Operation.Restore -> currentState.backupToRestore + ?.second + ?.let(restoreBackupUseCase::invoke) + ?.doOnSubscribe { updateState { it.copy(loading = true) } } + ?.subscribeOnMainThread(schedulersProvider) + ?.subscribeBy(::errorLog) { + updateState { it.copy(loading = false, complete = true) } + } + ?.addToDisposable() + + null -> Unit + } + } + + is BackupIntent.SelectOperation -> updateState { + it.copy( + operation = when (intent.value) { + BackupState.Operation.Create::class.java.name -> { + BackupState.Operation.Create() + } + else -> BackupState.Operation.Restore() + }, + ) + } + + is BackupIntent.ToggleBackupEntry -> updateState { state -> + when (state.operation) { + is BackupState.Operation.Create -> state.copy( + operation = state.operation.copy( + tokens = state.operation.tokens.map { (entry, selected) -> + if (intent.entry == entry) entry to intent.checked + else entry to selected + }, + ), + ) + + is BackupState.Operation.Restore -> state + null -> state + } + } + + BackupIntent.OnResult.Fail -> updateState { + it.copy( + screenModal = Modal.Error("Error creating backup".asUiText()), + loading = false, + ) + } + + BackupIntent.OnResult.Success -> updateState { + it.copy(complete = true, loading = false) + } + + BackupIntent.DismissModal -> updateState { + it.copy(screenModal = Modal.None) + } + + is BackupIntent.SelectRestore -> updateState { + it.copy(backupToRestore = intent.path to intent.bytes) + } + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/components/BackupEntriesComponent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/components/BackupEntriesComponent.kt new file mode 100644 index 00000000..9986b8f9 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/components/BackupEntriesComponent.kt @@ -0,0 +1,58 @@ +package com.shifthackz.aisdv1.presentation.screen.backup.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.core.localization.R as LocalizationR +import com.shifthackz.aisdv1.domain.entity.BackupEntryToken + +@Composable +fun BackupEntriesComponent( + modifier: Modifier = Modifier, + tokens: List> = listOf(), + onCheckedChange: (BackupEntryToken, Boolean) -> Unit = { _, _ -> }, +) { + Column( + modifier = modifier, + ) { + tokens.forEach { (entry, checked) -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Checkbox( + checked = checked, + onCheckedChange = { + onCheckedChange(entry, it) + }, + ) + Text( + text = stringResource( + id = when (entry) { + BackupEntryToken.AppConfiguration -> LocalizationR.string.backup_entry_prefs + BackupEntryToken.Gallery -> LocalizationR.string.backup_entry_gallery + } + ), + ) + } + } + } +} + +@Composable +@Preview +private fun BackupEntriesComponentPreview() { + Column { + BackupEntriesComponent( + tokens = BackupEntryToken.entries.map { it to false }, + ) + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/components/BackupOperationButton.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/components/BackupOperationButton.kt new file mode 100644 index 00000000..5b4e5484 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/components/BackupOperationButton.kt @@ -0,0 +1,86 @@ +package com.shifthackz.aisdv1.presentation.screen.backup.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Backup +import androidx.compose.material.icons.filled.Restore +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.presentation.screen.backup.BackupState +import kotlin.reflect.KClass +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +@Composable +fun BackupOperationButton( + state: BackupState, + operationClass: String, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(16.dp), + ) + .border( + width = 2.dp, + shape = RoundedCornerShape(16.dp), +// color = if (state.operation == operationClass) MaterialTheme.colorScheme.primary +// else Color.Transparent, + color = state.operation + ?.takeIf { it::class.java.name == operationClass } + ?.let { + MaterialTheme.colorScheme.primary + } ?: Color.Transparent + ) + .clickable { onClick() } + .padding(horizontal = 4.dp) + .padding(bottom = 4.dp), + ) { + Row { + Icon( + modifier = Modifier + .size(42.dp) + .padding(top = 8.dp, bottom = 8.dp), + imageVector = when (operationClass) { + BackupState.Operation.Create::class.java.name -> Icons.Default.Backup + BackupState.Operation.Restore::class.java.name -> Icons.Default.Restore + else -> Icons.Default.Restore + }, + contentDescription = null, + ) + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(top = 8.dp, bottom = 8.dp), + text = stringResource( + id = when (operationClass) { + BackupState.Operation.Create::class.java.name-> LocalizationR.string.backup_operation_create + BackupState.Operation.Restore::class.java.name -> LocalizationR.string.backup_operation_restore + else -> LocalizationR.string.backup_operation_restore + } + ), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + ) + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/BackupOperationForm.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/BackupOperationForm.kt new file mode 100644 index 00000000..a27b5c41 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/BackupOperationForm.kt @@ -0,0 +1,40 @@ +package com.shifthackz.aisdv1.presentation.screen.backup.forms + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.presentation.screen.backup.BackupIntent +import com.shifthackz.aisdv1.presentation.screen.backup.BackupState +import com.shifthackz.aisdv1.presentation.screen.backup.components.BackupOperationButton + +@Composable +fun BackupOperationForm( + state: BackupState, + processIntent: (BackupIntent) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(top = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + + listOf( + BackupState.Operation.Create::class.java.name, + BackupState.Operation.Restore::class.java.name, + ).forEach { item -> + BackupOperationButton( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + state = state, + operationClass = item, + ) { + processIntent(BackupIntent.SelectOperation(item)) + } + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/CreateBackupForm.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/CreateBackupForm.kt new file mode 100644 index 00000000..1467393b --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/CreateBackupForm.kt @@ -0,0 +1,26 @@ +package com.shifthackz.aisdv1.presentation.screen.backup.forms + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.shifthackz.aisdv1.presentation.screen.backup.BackupIntent +import com.shifthackz.aisdv1.presentation.screen.backup.BackupState +import com.shifthackz.aisdv1.presentation.screen.backup.components.BackupEntriesComponent + +@Composable +fun CreateBackupForm( + state: BackupState, + processIntent: (BackupIntent) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + BackupEntriesComponent( + tokens = (state.operation as BackupState.Operation.Create).tokens, + onCheckedChange = { entry, checked -> + processIntent(BackupIntent.ToggleBackupEntry(entry, checked)) + }, + ) + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/RestoreBackupForm.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/RestoreBackupForm.kt new file mode 100644 index 00000000..b975dd53 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/backup/forms/RestoreBackupForm.kt @@ -0,0 +1,115 @@ +package com.shifthackz.aisdv1.presentation.screen.backup.forms + +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.core.common.file.LOCAL_DIFFUSION_CUSTOM_PATH +import com.shifthackz.aisdv1.core.localization.R +import com.shifthackz.aisdv1.core.model.asString +import com.shifthackz.aisdv1.presentation.screen.backup.BackupIntent +import com.shifthackz.aisdv1.presentation.screen.backup.BackupState +import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupIntent +import com.shifthackz.aisdv1.presentation.theme.textFieldColors +import com.shifthackz.aisdv1.presentation.utils.readByteArrayFromUri + +@Composable +fun RestoreBackupForm( + state: BackupState, + processIntent: (BackupIntent) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + val pickFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + onResult = { uri -> + uri ?: return@rememberLauncherForActivityResult + readByteArrayFromUri(context, uri)?.let { + processIntent(BackupIntent.SelectRestore("${uri.path}", it)) + } + } + ) + + Column( + modifier = modifier.padding(horizontal = 16.dp), + ) { + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 8.dp), + text = stringResource(id = R.string.model_local_path_header), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + ) + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 14.dp), + value = state.backupToRestore?.first ?: "", + onValueChange = {}, + readOnly = true, + enabled = true, + label = { Text(stringResource(R.string.model_local_path_title)) }, +// trailingIcon = { +// IconButton( +// onClick = { +// processIntent( +// ServerSetupIntent.SelectLocalModelPath(LOCAL_DIFFUSION_CUSTOM_PATH) +// ) +// }, +// content = { +// Icon( +// imageVector = Icons.Default.Refresh, +// contentDescription = "Reset", +// ) +// }, +// ) +// }, +// isError = state.localCustomModelPathValidationError != null, +// supportingText = { +// state.localCustomModelPathValidationError +// ?.let { Text(it.asString(), color = MaterialTheme.colorScheme.error) } +// }, + colors = textFieldColors, + ) + OutlinedButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp, bottom = 8.dp), + onClick = { + pickFileLauncher.launch(arrayOf("application/octet-stream")) + }, + ) { + Text( +// text = stringResource(id = R.string.model_local_path_button), + text = "Select file", + color = LocalContentColor.current, + ) + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsIntent.kt index 41f68563..36dd7965 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsIntent.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsIntent.kt @@ -13,6 +13,8 @@ sealed interface SettingsIntent : MviIntent { data object NavigateConfiguration : SettingsIntent + data object NavigateBackup : SettingsIntent + data object NavigateDeveloperMode : SettingsIntent sealed interface SdModel : SettingsIntent { diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsScreen.kt index 1942f01c..d53c4500 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsScreen.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountTree import androidx.compose.material.icons.filled.AllInclusive import androidx.compose.material.icons.filled.AutoFixNormal +import androidx.compose.material.icons.filled.Backup import androidx.compose.material.icons.filled.Circle import androidx.compose.material.icons.filled.Code import androidx.compose.material.icons.filled.ColorLens @@ -124,7 +125,9 @@ fun SettingsScreen() { } SettingsEffect.ShareLogFile -> ReportProblemEmailComposer().invoke(context) + is SettingsEffect.OpenUrl -> context.openUrl(effect.url) + SettingsEffect.DeveloperModeUnlocked -> context.showToast( LocalizationR.string.debug_action_unlock, ) @@ -219,6 +222,8 @@ private fun ContentSettingsState( .fillMaxWidth() .padding(top = 4.dp, start = 4.dp) + Spacer(modifier = Modifier.height(56.dp)) + if (!state.onBoardingDemo) { //region MAIN SETTINGS SettingsHeader( @@ -243,6 +248,13 @@ private fun ContentSettingsState( }.asUiText(), onClick = { processIntent(SettingsIntent.NavigateConfiguration) }, ) + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.Backup, + text = LocalizationR.string.title_backup.asUiText(), + onClick = { processIntent(SettingsIntent.NavigateBackup) }, + ) if (state.showStabilityAiCredits) SettingsItem( modifier = itemModifier, loading = state.loading, diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModel.kt index d7756582..bf270f66 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModel.kt @@ -118,6 +118,8 @@ class SettingsViewModel( LaunchSource.SETTINGS ) + SettingsIntent.NavigateBackup -> mainRouter.navigateToBackup() + SettingsIntent.NavigateDeveloperMode -> mainRouter.navigateToDebugMenu() SettingsIntent.SdModel.OpenChooser -> updateState { diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupScreen.kt index 0a77f3ad..52245224 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupScreen.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupScreen.kt @@ -43,13 +43,14 @@ import androidx.compose.ui.unit.dp import com.shifthackz.aisdv1.core.common.appbuild.BuildInfoProvider import com.shifthackz.aisdv1.core.common.extensions.openUrl import com.shifthackz.aisdv1.core.common.extensions.showToast +import com.shifthackz.aisdv1.core.model.asUiText import com.shifthackz.android.core.mvi.MviComponent import com.shifthackz.aisdv1.domain.entity.ServerSource import com.shifthackz.aisdv1.presentation.modal.ModalRenderer -import com.shifthackz.aisdv1.presentation.screen.setup.components.ConfigurationStepBar import com.shifthackz.aisdv1.presentation.screen.setup.steps.ConfigurationStep import com.shifthackz.aisdv1.presentation.screen.setup.steps.SourceSelectionStep import com.shifthackz.aisdv1.presentation.utils.PermissionUtil +import com.shifthackz.aisdv1.presentation.widget.toolbar.StepBar import com.shifthackz.aisdv1.core.localization.R as LocalizationR @Composable @@ -139,7 +140,15 @@ fun ServerSetupScreenContent( WindowInsets(0, 0, 0, 0) }, ) - ConfigurationStepBar(currentStep = state.step) + StepBar( + steps = ServerSetupState.Step.entries, + currentStep = state.step, + ) { step -> + when (step) { + ServerSetupState.Step.SOURCE -> LocalizationR.string.srv_step_1 + ServerSetupState.Step.CONFIGURE -> LocalizationR.string.srv_step_2 + }.asUiText() + } } }, bottomBar = { diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/UriToBitmap.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/UriToBitmap.kt deleted file mode 100644 index 56930b01..00000000 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/UriToBitmap.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.shifthackz.aisdv1.presentation.utils - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri -import com.shifthackz.aisdv1.core.common.log.errorLog - -fun uriToBitmap(context: Context, uri: Uri): Bitmap? = try { - val inputStream = context.contentResolver.openInputStream(uri) - BitmapFactory.decodeStream(inputStream).also { inputStream?.close() } -} catch (e: Exception) { - errorLog("UrlToBitmap", e) - null -} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/UriUtils.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/UriUtils.kt new file mode 100644 index 00000000..046fefb6 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/UriUtils.kt @@ -0,0 +1,32 @@ +package com.shifthackz.aisdv1.presentation.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import com.shifthackz.aisdv1.core.common.log.errorLog + +fun uriToBitmap(context: Context, uri: Uri): Bitmap? = try { + val inputStream = context.contentResolver.openInputStream(uri) + BitmapFactory.decodeStream(inputStream).also { inputStream?.close() } +} catch (e: Exception) { + errorLog("UrlToBitmap", e) + null +} + +fun saveByteArrayToUri(context: Context, uri: Uri, byteArray: ByteArray) = try { + context.contentResolver.openOutputStream(uri)?.use { outputStream -> + outputStream.write(byteArray) + } +} catch (e: Exception) { + errorLog("SaveByteArrayToUri", e) +} + +fun readByteArrayFromUri(context: Context, uri: Uri): ByteArray? = try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + inputStream.readBytes() + } +} catch (e: Exception) { + errorLog("ReadByteArrayFromUri", e) + null +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/components/ConfigurationStepBar.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/toolbar/StepBar.kt similarity index 73% rename from presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/components/ConfigurationStepBar.kt rename to presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/toolbar/StepBar.kt index 8b127592..bfdb5167 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/components/ConfigurationStepBar.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/toolbar/StepBar.kt @@ -1,4 +1,4 @@ -package com.shifthackz.aisdv1.presentation.screen.setup.components +package com.shifthackz.aisdv1.presentation.widget.toolbar import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -22,16 +22,20 @@ 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.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.core.model.UiText +import com.shifthackz.aisdv1.core.model.asString +import com.shifthackz.aisdv1.core.model.asUiText import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupState import com.shifthackz.aisdv1.core.localization.R as LocalizationR @Composable -fun ConfigurationStepBar( +fun StepBar( modifier: Modifier = Modifier, - currentStep: ServerSetupState.Step, + steps: List, + currentStep: T, + displayDelegate: (T) -> UiText, ) { val circleSize = 36.dp val circleBorder = 2.dp @@ -55,22 +59,24 @@ fun ConfigurationStepBar( .height(lineHeight) .fillMaxWidth() Box(modifier = lineMod.weight(0.5f)) - (0 until ServerSetupState.Step.entries.size - 1).forEach { index -> + (0 until steps.size - 1).forEach { index -> Box( modifier = lineMod .weight(1f) .padding(horizontal = circleSize / 2 - circleBorder / 2) - .background(color = when { - currentStep.ordinal > index -> colorAccent - else -> colorBg - }), + .background( + color = when { + steps.indexOf(currentStep) > index -> colorAccent + else -> colorBg + } + ), ) } Box(modifier = lineMod.weight(0.5f)) } Row { val localModifier = Modifier.weight(1f) - ServerSetupState.Step.entries.forEach { step -> + steps.forEachIndexed { index, step -> val localShape = CircleShape Column( modifier = localModifier, @@ -84,7 +90,7 @@ fun ConfigurationStepBar( .background(color = colorBg) .border( width = when { - step.ordinal <= currentStep.ordinal -> circleBorder + index <= steps.indexOf(currentStep) -> circleBorder else -> circleBorder / 2 }, color = colorAccent, @@ -93,8 +99,8 @@ fun ConfigurationStepBar( contentAlignment = Alignment.Center ) { val icon = when { - step.ordinal < currentStep.ordinal -> Icons.Default.Check - step.ordinal == currentStep.ordinal -> Icons.Default.Circle + index < steps.indexOf(currentStep) -> Icons.Default.Check + index == steps.indexOf(currentStep) -> Icons.Default.Circle else -> null } icon?.let { @@ -107,14 +113,9 @@ fun ConfigurationStepBar( } Text( modifier = Modifier.padding(top = 8.dp), - text = stringResource( - id = when (step) { - ServerSetupState.Step.SOURCE -> LocalizationR.string.srv_step_1 - ServerSetupState.Step.CONFIGURE -> LocalizationR.string.srv_step_2 - } - ), + text = displayDelegate(step).asString(), style = MaterialTheme.typography.labelSmall, - color = if (step.ordinal == currentStep.ordinal) { + color = if (step == currentStep) { colorAccent } else { Color.Unspecified @@ -129,13 +130,19 @@ fun ConfigurationStepBar( @Composable @Preview -private fun PreviewStep1() { - ConfigurationStepBar(currentStep = ServerSetupState.Step.SOURCE) - -} - -@Composable -@Preview -private fun PreviewStep2() { - ConfigurationStepBar(currentStep = ServerSetupState.Step.CONFIGURE) +private fun PreviewServerSetupStepBar() { + Column { + ServerSetupState.Step.entries.forEach { step -> + StepBar( + steps = ServerSetupState.Step.entries, + currentStep = step, + displayDelegate = { currentStep -> + when (currentStep) { + ServerSetupState.Step.SOURCE -> LocalizationR.string.srv_step_1 + ServerSetupState.Step.CONFIGURE -> LocalizationR.string.srv_step_2 + }.asUiText() + }, + ) + } + } } diff --git a/storage/src/main/java/com/shifthackz/aisdv1/storage/db/persistent/dao/GenerationResultDao.kt b/storage/src/main/java/com/shifthackz/aisdv1/storage/db/persistent/dao/GenerationResultDao.kt index 8b39a965..cb245d5d 100644 --- a/storage/src/main/java/com/shifthackz/aisdv1/storage/db/persistent/dao/GenerationResultDao.kt +++ b/storage/src/main/java/com/shifthackz/aisdv1/storage/db/persistent/dao/GenerationResultDao.kt @@ -27,6 +27,9 @@ interface GenerationResultDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(item: GenerationResultEntity): Single + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(items: List): Completable + @Query("DELETE FROM ${GenerationResultContract.TABLE} WHERE ${GenerationResultContract.ID} = :id") fun deleteById(id: Long): Completable