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