diff --git a/core/localization/src/main/res/values-ru/strings.xml b/core/localization/src/main/res/values-ru/strings.xml index a9dd642b..acbaa8e7 100644 --- a/core/localization/src/main/res/values-ru/strings.xml +++ b/core/localization/src/main/res/values-ru/strings.xml @@ -218,6 +218,7 @@ Политика конфиденциальности Как настроить сервер Поддержать проект + Просмотреть руководство Исходный код Подождите, коммуницируем с сервером… @@ -353,4 +354,9 @@ I/O Поток Вычислительный Однопоточность + + Расширенные функции\n[Stable Diffusion]. + [Офлайн] генерация\nLocal Diffusion. + Настройте приложение,\nсделайте его [своим]! + [Свобода] выбора\nпровайедра генерации. diff --git a/core/localization/src/main/res/values-tr/strings.xml b/core/localization/src/main/res/values-tr/strings.xml index 815b72d9..eed6dea1 100644 --- a/core/localization/src/main/res/values-tr/strings.xml +++ b/core/localization/src/main/res/values-tr/strings.xml @@ -218,6 +218,7 @@ Gizlilik ve Hukuksal Politikalar Sunucu kurulum talimatları Bağış yapmak + Eğitimi görüntüle Kaynak kodunu alın Lütfen bekleyin, sunucu ile iletişime geçiliyor… @@ -353,4 +354,9 @@ G/Ç İş Parçacığı Hesaplama Tek İş Parçacığı + + Gelişmiş Özellikler\n[Kararlı Dağıtım]. + [Çevrimdışı] nesil\nYerel Dağıtım. + Uygulamayı özelleştirin,\n[kendinizin] yapın! + Nesil sağlayıcıyı\nseçme [Özgürlüğü]. diff --git a/core/localization/src/main/res/values-uk/strings.xml b/core/localization/src/main/res/values-uk/strings.xml index 1ff9adf6..da32118e 100644 --- a/core/localization/src/main/res/values-uk/strings.xml +++ b/core/localization/src/main/res/values-uk/strings.xml @@ -218,6 +218,7 @@ Політика конфіденційності Як налаштувати сервер Підтримати проект + Переглянути туторіал Вихідний код Зачекайте, йде коммунікація із сервером… @@ -353,4 +354,9 @@ I/O Потік Обчислення Один потік + + Розширені можливості\n[Stable Diffusion]. + [Офлайн] генерація\nLocal Diffusion. + Налаштуйте додаток,\nзробіть його [своїм]! + [Свобода] вибору\nпостачальника генерації. diff --git a/core/localization/src/main/res/values-zh/strings.xml b/core/localization/src/main/res/values-zh/strings.xml index 2199fbcd..c3483d2e 100644 --- a/core/localization/src/main/res/values-zh/strings.xml +++ b/core/localization/src/main/res/values-zh/strings.xml @@ -264,6 +264,7 @@ 隐私和法律政策 服务器设置说明 捐赠 + 查看教程 获取源代码 @@ -419,4 +420,9 @@ I/O 线程 计算 单线程 + + 高级功能\n[稳定扩散]。 + [离线]生成\n本地扩散。 + 自定义应用程序,\n让它成为[您的]! + [自由]选择\n代提供商。 diff --git a/core/localization/src/main/res/values/strings.xml b/core/localization/src/main/res/values/strings.xml index f455448f..cba23819 100755 --- a/core/localization/src/main/res/values/strings.xml +++ b/core/localization/src/main/res/values/strings.xml @@ -238,6 +238,7 @@ Privacy and legal policy Server setup instructions Donate + View tutorial Get source code Hang tight, communicating with server… @@ -377,4 +378,9 @@ I/O Thread Computation Single Thread + + Advanced [Stable Diffusion]\nAI generation features. + [Offline] Local Diffusion\nAI generation. + Configure, customize,\nmake it [yours]! + [Freedom] to choose your\nAI generation provider. diff --git a/core/ui/src/main/java/com/shifthackz/aisdv1/core/extensions/GestureExtensions.kt b/core/ui/src/main/java/com/shifthackz/aisdv1/core/extensions/GestureExtensions.kt new file mode 100644 index 00000000..277a1cff --- /dev/null +++ b/core/ui/src/main/java/com/shifthackz/aisdv1/core/extensions/GestureExtensions.kt @@ -0,0 +1,22 @@ +package com.shifthackz.aisdv1.core.extensions + +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput + +fun Modifier.gesturesDisabled(disabled: Boolean = true) = + if (disabled) { + pointerInput(Unit) { + awaitPointerEventScope { + // we should wait for all new pointer events + while (true) { + awaitPointerEvent(pass = PointerEventPass.Initial) + .changes + .forEach(PointerInputChange::consume) + } + } + } + } else { + this + } diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/preference/PreferenceManagerImpl.kt b/data/src/main/java/com/shifthackz/aisdv1/data/preference/PreferenceManagerImpl.kt index 1fffb4d4..e88fcf47 100644 --- a/data/src/main/java/com/shifthackz/aisdv1/data/preference/PreferenceManagerImpl.kt +++ b/data/src/main/java/com/shifthackz/aisdv1/data/preference/PreferenceManagerImpl.kt @@ -183,6 +183,12 @@ class PreferenceManagerImpl( .apply() .also { onPreferencesChanged() } + override var onBoardingComplete: Boolean + get() = preferences.getBoolean(KEY_ON_BOARDING_COMPLETE, false) + set(value) = preferences.edit() + .putBoolean(KEY_ON_BOARDING_COMPLETE, value) + .apply() + override var forceSetupAfterUpdate: Boolean get() = preferences.getBoolean(KEY_FORCE_SETUP_AFTER_UPDATE, true) set(value) = preferences.edit() @@ -311,6 +317,7 @@ class PreferenceManagerImpl( const val KEY_HUGGING_FACE_MODEL_KEY = "key_hugging_face_model_key" const val KEY_STABILITY_AI_API_KEY = "key_stability_ai_api_key" const val KEY_STABILITY_AI_ENGINE_ID_KEY = "key_stability_ai_engine_id_key" + const val KEY_ON_BOARDING_COMPLETE = "key_on_boarding_complete" const val KEY_FORCE_SETUP_AFTER_UPDATE = "force_upd_setup_v0.x.x-v0.6.2" const val KEY_LOCAL_MODEL_ID = "key_local_model_id" const val KEY_LOCAL_NN_API = "key_local_nn_api" diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/preference/PreferenceManager.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/preference/PreferenceManager.kt index 50e0cda5..1ee8950b 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/preference/PreferenceManager.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/preference/PreferenceManager.kt @@ -28,6 +28,7 @@ interface PreferenceManager { var huggingFaceModel: String var stabilityAiApiKey: String var stabilityAiEngineId: String + var onBoardingComplete: Boolean var forceSetupAfterUpdate: Boolean var localModelId: String var localUseNNAPI: Boolean diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/splash/SplashNavigationUseCaseImpl.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/splash/SplashNavigationUseCaseImpl.kt index 2dbb2831..338f7a97 100644 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/splash/SplashNavigationUseCaseImpl.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/splash/SplashNavigationUseCaseImpl.kt @@ -11,6 +11,10 @@ internal class SplashNavigationUseCaseImpl( override fun invoke(): Single = Single.create { emitter -> val action = when { + !preferenceManager.onBoardingComplete -> { + Action.LAUNCH_ONBOARDING + } + preferenceManager.forceSetupAfterUpdate -> { Action.LAUNCH_SERVER_SETUP } diff --git a/domain/src/test/java/com/shifthackz/aisdv1/domain/usecase/splash/SplashNavigationUseCaseImplTest.kt b/domain/src/test/java/com/shifthackz/aisdv1/domain/usecase/splash/SplashNavigationUseCaseImplTest.kt index cb76cb84..1c2b3a0d 100644 --- a/domain/src/test/java/com/shifthackz/aisdv1/domain/usecase/splash/SplashNavigationUseCaseImplTest.kt +++ b/domain/src/test/java/com/shifthackz/aisdv1/domain/usecase/splash/SplashNavigationUseCaseImplTest.kt @@ -12,8 +12,22 @@ class SplashNavigationUseCaseImplTest { private val useCase = SplashNavigationUseCaseImpl(stubPreferenceManager) + @Test + fun `given onBoardingComplete is false, expected LAUNCH_ONBOARDING`() { + whenever(stubPreferenceManager.onBoardingComplete) + .thenReturn(false) + + useCase() + .test() + .assertNoErrors() + .assertValue(SplashNavigationUseCase.Action.LAUNCH_ONBOARDING) + } + @Test fun `given forceSetupAfterUpdate is true, expected LAUNCH_SERVER_SETUP`() { + whenever(stubPreferenceManager.onBoardingComplete) + .thenReturn(true) + whenever(stubPreferenceManager.forceSetupAfterUpdate) .thenReturn(true) @@ -25,6 +39,9 @@ class SplashNavigationUseCaseImplTest { @Test fun `given source is AUTOMATIC1111 and server url empty, expected LAUNCH_SERVER_SETUP`() { + whenever(stubPreferenceManager.onBoardingComplete) + .thenReturn(true) + whenever(stubPreferenceManager.forceSetupAfterUpdate) .thenReturn(false) @@ -42,6 +59,9 @@ class SplashNavigationUseCaseImplTest { @Test fun `given source is AUTOMATIC1111 and server url not empty, expected LAUNCH_HOME`() { + whenever(stubPreferenceManager.onBoardingComplete) + .thenReturn(true) + whenever(stubPreferenceManager.forceSetupAfterUpdate) .thenReturn(false) @@ -59,6 +79,9 @@ class SplashNavigationUseCaseImplTest { @Test fun `given source is LOCAL, and server url is empty, expected LAUNCH_HOME`() { + whenever(stubPreferenceManager.onBoardingComplete) + .thenReturn(true) + whenever(stubPreferenceManager.forceSetupAfterUpdate) .thenReturn(false) @@ -76,6 +99,9 @@ class SplashNavigationUseCaseImplTest { @Test fun `given source is LOCAL, and server url is not empty, expected LAUNCH_HOME`() { + whenever(stubPreferenceManager.onBoardingComplete) + .thenReturn(true) + whenever(stubPreferenceManager.forceSetupAfterUpdate) .thenReturn(false) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviState.kt index e1fbb7d5..7e8f7130 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviState.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviState.kt @@ -12,6 +12,7 @@ import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.android.core.mvi.MviState abstract class GenerationMviState : MviState { + abstract val onBoardingDemo: Boolean abstract val screenModal: Modal abstract val mode: ServerSource abstract val advancedToggleButtonVisible: Boolean @@ -55,6 +56,7 @@ abstract class GenerationMviState : MviState { get() = widthValidationError != null || heightValidationError != null open fun copyState( + onBoardingDemo: Boolean = this.onBoardingDemo, screenModal: Modal = this.screenModal, mode: ServerSource = this.mode, advancedToggleButtonVisible: Boolean = this.advancedToggleButtonVisible, diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviViewModel.kt index cea05b90..714f0ef9 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviViewModel.kt @@ -22,11 +22,11 @@ import com.shifthackz.aisdv1.domain.usecase.generation.ObserveHordeProcessStatus import com.shifthackz.aisdv1.domain.usecase.generation.ObserveLocalDiffusionProcessStatusUseCase import com.shifthackz.aisdv1.domain.usecase.generation.SaveGenerationResultUseCase import com.shifthackz.aisdv1.domain.usecase.sdsampler.GetStableDiffusionSamplersUseCase +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.navigation.router.drawer.DrawerRouter import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter import com.shifthackz.aisdv1.presentation.screen.drawer.DrawerIntent -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.screen.txt2img.mapToUi import com.shifthackz.android.core.mvi.MviEffect import io.reactivex.rxjava3.core.Observable @@ -262,7 +262,7 @@ abstract class GenerationMviViewModel mainRouter.navigateToServerSetup( - ServerSetupLaunchSource.SETTINGS, + LaunchSource.SETTINGS, ) is GenerationMviIntent.UpdateFromGeneration -> { 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 a22190ec..23e5a154 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 @@ -5,6 +5,7 @@ import com.shifthackz.aisdv1.presentation.modal.embedding.EmbeddingViewModel 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.debug.DebugMenuViewModel import com.shifthackz.aisdv1.presentation.screen.donate.DonateViewModel import com.shifthackz.aisdv1.presentation.screen.drawer.DrawerViewModel @@ -15,8 +16,8 @@ import com.shifthackz.aisdv1.presentation.screen.img2img.ImageToImageViewModel import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintViewModel import com.shifthackz.aisdv1.presentation.screen.loader.ConfigurationLoaderViewModel import com.shifthackz.aisdv1.presentation.screen.logger.LoggerViewModel +import com.shifthackz.aisdv1.presentation.screen.onboarding.OnBoardingViewModel import com.shifthackz.aisdv1.presentation.screen.settings.SettingsViewModel -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupViewModel import com.shifthackz.aisdv1.presentation.screen.splash.SplashViewModel import com.shifthackz.aisdv1.presentation.screen.txt2img.TextToImageViewModel @@ -54,7 +55,17 @@ val viewModelModule = module { viewModelOf(::LoggerViewModel) viewModel { parameters -> - val launchSource = ServerSetupLaunchSource.fromKey(parameters.get()) + OnBoardingViewModel( + launchSource = LaunchSource.fromKey(parameters.get()), + mainRouter = get(), + splashNavigationUseCase = get(), + preferenceManager = get(), + schedulersProvider = get(), + ) + } + + viewModel { parameters -> + val launchSource = LaunchSource.fromKey(parameters.get()) ServerSetupViewModel( launchSource = launchSource, getConfigurationUseCase = get(), diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/LaunchSource.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/LaunchSource.kt new file mode 100644 index 00000000..14fb1545 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/LaunchSource.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.presentation.model + +enum class LaunchSource { + SPLASH, + SETTINGS; + + companion object { + fun fromKey(key: Int) = entries.firstOrNull { it.ordinal == key } ?: SPLASH + } +} 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 b5f0bfbe..5146f51d 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 @@ -9,8 +9,8 @@ import com.shifthackz.aisdv1.core.model.asUiText import com.shifthackz.aisdv1.domain.entity.FeatureTag import com.shifthackz.aisdv1.domain.entity.ServerSource import com.shifthackz.aisdv1.domain.entity.Settings +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.model.NavItem -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.utils.Constants import com.shifthackz.aisdv1.presentation.widget.source.getNameUiText import com.shifthackz.aisdv1.core.localization.R as LocalizationR @@ -44,7 +44,7 @@ private fun webUi(source: ServerSource) = NavItem( private fun configuration() = NavItem( name = LocalizationR.string.settings_item_config.asUiText(), - route = "${Constants.ROUTE_SERVER_SETUP}/${ServerSetupLaunchSource.SETTINGS.ordinal}", + route = "${Constants.ROUTE_SERVER_SETUP}/${LaunchSource.SETTINGS.ordinal}", icon = NavItem.Icon.Vector( vector = Icons.Default.SettingsEthernet, ), 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 ad5a352d..1c04b2a3 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 @@ -5,13 +5,15 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.compose.ComposeNavigator import androidx.navigation.get +import com.shifthackz.aisdv1.presentation.model.LaunchSource 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 import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintScreen import com.shifthackz.aisdv1.presentation.screen.loader.ConfigurationLoaderScreen import com.shifthackz.aisdv1.presentation.screen.logger.LoggerScreen -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource +import com.shifthackz.aisdv1.presentation.screen.onboarding.OnBoardingScreen +import com.shifthackz.aisdv1.presentation.screen.onboarding.OnBoardingViewModel import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupScreen import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupViewModel import com.shifthackz.aisdv1.presentation.screen.splash.SplashScreen @@ -33,7 +35,7 @@ fun NavGraphBuilder.mainNavGraph() { ComposeNavigator.Destination(provider[ComposeNavigator::class]) { entry -> val sourceKey = entry.arguments ?.getInt(Constants.PARAM_SOURCE) - ?: ServerSetupLaunchSource.SPLASH.ordinal + ?: LaunchSource.SPLASH.ordinal ServerSetupScreen( viewModel = getViewModel( parameters = { parametersOf(sourceKey) } @@ -103,4 +105,22 @@ fun NavGraphBuilder.mainNavGraph() { route = Constants.ROUTE_DONATE } ) + addDestination( + ComposeNavigator.Destination(provider[ComposeNavigator::class]) { entry -> + val sourceKey = entry.arguments + ?.getInt(Constants.PARAM_SOURCE) + ?: LaunchSource.SPLASH.ordinal + OnBoardingScreen( + viewModel = getViewModel( + parameters = { parametersOf(sourceKey) } + ), + ) + }.apply { + route = Constants.ROUTE_ONBOARDING_FULL + addArgument( + Constants.PARAM_SOURCE, + NavArgument.Builder().setType(NavType.IntType).build(), + ) + } + ) } 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 7f64a566..b45a025c 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 @@ -1,18 +1,20 @@ package com.shifthackz.aisdv1.presentation.navigation.router.main +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.navigation.NavigationEffect import com.shifthackz.aisdv1.presentation.navigation.router.Router -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource interface MainRouter : Router { fun navigateBack() + fun navigateToOnBoarding(source: LaunchSource) + fun navigateToPostSplashConfigLoader() fun navigateToHomeScreen() - fun navigateToServerSetup(source: ServerSetupLaunchSource) + fun navigateToServerSetup(source: LaunchSource) fun navigateToGalleryDetails(itemId: Long) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterExtensions.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterExtensions.kt new file mode 100644 index 00000000..ed0df2b9 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterExtensions.kt @@ -0,0 +1,18 @@ +package com.shifthackz.aisdv1.presentation.navigation.router.main + +import com.shifthackz.aisdv1.domain.usecase.splash.SplashNavigationUseCase +import com.shifthackz.aisdv1.presentation.model.LaunchSource + +fun MainRouter.postSplashNavigation( + action: SplashNavigationUseCase.Action, +) { + when (action) { + SplashNavigationUseCase.Action.LAUNCH_ONBOARDING -> navigateToOnBoarding( + source = LaunchSource.SPLASH, + ) + SplashNavigationUseCase.Action.LAUNCH_SERVER_SETUP -> navigateToServerSetup( + source = LaunchSource.SPLASH, + ) + SplashNavigationUseCase.Action.LAUNCH_HOME -> navigateToPostSplashConfigLoader() + } +} 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 24a8c52c..df0b16af 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 @@ -1,15 +1,12 @@ package com.shifthackz.aisdv1.presentation.navigation.router.main +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.navigation.NavigationEffect -import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuAccessor -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.utils.Constants import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.subjects.PublishSubject -internal class MainRouterImpl( - private val debugMenuAccessor: DebugMenuAccessor, -) : MainRouter { +internal class MainRouterImpl : MainRouter { private val effectSubject: PublishSubject = PublishSubject.create() @@ -21,6 +18,16 @@ internal class MainRouterImpl( effectSubject.onNext(NavigationEffect.Back) } + override fun navigateToOnBoarding(source: LaunchSource) { + effectSubject.onNext(NavigationEffect.Navigate.RouteBuilder("${Constants.ROUTE_ONBOARDING}/${source.ordinal}") { + if (source == LaunchSource.SPLASH) { + popUpTo(Constants.ROUTE_SPLASH) { + inclusive = true + } + } + }) + } + override fun navigateToPostSplashConfigLoader() { effectSubject.onNext(NavigationEffect.Navigate.RouteBuilder(Constants.ROUTE_CONFIG_LOADER) { popUpTo(Constants.ROUTE_SPLASH) { @@ -30,13 +37,17 @@ internal class MainRouterImpl( } override fun navigateToHomeScreen() { - effectSubject.onNext(NavigationEffect.Navigate.RoutePopUp(Constants.ROUTE_HOME)) + effectSubject.onNext(NavigationEffect.Navigate.RouteBuilder(Constants.ROUTE_HOME) { + popUpTo(0) { + inclusive = true + } + }) } - override fun navigateToServerSetup(source: ServerSetupLaunchSource) { + override fun navigateToServerSetup(source: LaunchSource) { effectSubject.onNext(NavigationEffect.Navigate.RouteBuilder("${Constants.ROUTE_SERVER_SETUP}/${source.ordinal}") { - if (source == ServerSetupLaunchSource.SPLASH) { - popUpTo(Constants.ROUTE_SPLASH) { + if (source == LaunchSource.SPLASH) { + popUpTo(Constants.ROUTE_ONBOARDING) { inclusive = true } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/list/GalleryScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/list/GalleryScreen.kt index 8d36fb3d..a6052000 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/list/GalleryScreen.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/list/GalleryScreen.kt @@ -131,7 +131,7 @@ fun GalleryScreen() { BackHandler(state.selectionMode) { intentHandler(GalleryIntent.ChangeSelectionMode(false)) } - ScreenContent( + GalleryScreenContent( state = state, lazyGalleryItems = lazyGalleryItems, processIntent = intentHandler, @@ -140,7 +140,7 @@ fun GalleryScreen() { } @Composable -private fun ScreenContent( +fun GalleryScreenContent( modifier: Modifier = Modifier, state: GalleryState, lazyGalleryItems: LazyPagingItems, diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/img2img/ImageToImageState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/img2img/ImageToImageState.kt index 4732517e..533b7b9b 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/img2img/ImageToImageState.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/img2img/ImageToImageState.kt @@ -21,6 +21,7 @@ data class ImageToImageState( val imageBase64: String = "", val denoisingStrength: Float = 0.75f, val inPaintModel: InPaintModel = InPaintModel(), + override val onBoardingDemo: Boolean = false, override val screenModal: Modal = Modal.None, override val mode: ServerSource = ServerSource.AUTOMATIC1111, override val advancedToggleButtonVisible: Boolean = true, @@ -61,6 +62,7 @@ data class ImageToImageState( } override fun copyState( + onBoardingDemo: Boolean, screenModal: Modal, mode: ServerSource, advancedToggleButtonVisible: Boolean, @@ -90,6 +92,7 @@ data class ImageToImageState( batchCount: Int, generateButtonEnabled: Boolean ): GenerationMviState = copy( + onBoardingDemo = onBoardingDemo, screenModal = screenModal, mode = mode, advancedToggleButtonVisible = advancedToggleButtonVisible, diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingDensity.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingDensity.kt new file mode 100644 index 00000000..9ee89f1e --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingDensity.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.ui.unit.Density + +val onBoardingDensity = Density(2f, 1f) +val onBoardingPhoneWidthFraction = 0.76f +val onBoardingPhoneAspectRatio = 9.5f / 16f +val onBoardingPageAnimation = tween(1200, easing = FastOutSlowInEasing) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingIntent.kt new file mode 100644 index 00000000..f91f592c --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingIntent.kt @@ -0,0 +1,7 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding + +import com.shifthackz.android.core.mvi.MviIntent + +sealed interface OnBoardingIntent : MviIntent { + data object Navigate : OnBoardingIntent +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingPage.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingPage.kt new file mode 100644 index 00000000..560da13e --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingPage.kt @@ -0,0 +1,9 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding + + +enum class OnBoardingPage { + Providers, + Form, + LocalDiffusion, + LookAndFeel, +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingScreen.kt new file mode 100644 index 00000000..708a7bbc --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingScreen.kt @@ -0,0 +1,210 @@ +@file:OptIn(ExperimentalFoundationApi::class) + +package com.shifthackz.aisdv1.presentation.screen.onboarding + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState +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.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.DoubleArrow +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.core.ui.MviComponent +import com.shifthackz.aisdv1.presentation.model.LaunchSource +import com.shifthackz.aisdv1.presentation.screen.onboarding.page.FormPageContent +import com.shifthackz.aisdv1.presentation.screen.onboarding.page.LocalDiffusionPageContent +import com.shifthackz.aisdv1.presentation.screen.onboarding.page.LookAndFeelPageContent +import com.shifthackz.aisdv1.presentation.screen.onboarding.page.ProviderPageContent +import kotlinx.coroutines.launch + +@Composable +fun OnBoardingScreen( + viewModel: OnBoardingViewModel, +) { + MviComponent( + viewModel = viewModel, + navigationBarColor = MaterialTheme.colorScheme.surface, + applySystemUiColors = true, + ) { state, processIntent -> + OnBoardingScreenContent( + launchSource = viewModel.launchSource, + state = state, + processIntent = processIntent, + ) + } +} + +@Composable +private fun OnBoardingScreenContent( + launchSource: LaunchSource, + state: OnBoardingState, + processIntent: (OnBoardingIntent) -> Unit = {}, +) { + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = OnBoardingPage.entries.first().ordinal, + pageCount = { OnBoardingPage.entries.size }, + ) + BackHandler(pagerState.currentPage > 0) { + scope.launch { + pagerState.animateScrollToPage( + page = pagerState.currentPage - 1, + animationSpec = onBoardingPageAnimation, + ) + } + } + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + val shape = RoundedCornerShape( + topStart = 24.dp, + topEnd = 24.dp, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .clip(shape), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier + .fillMaxWidth(onBoardingPhoneWidthFraction) + .padding(bottom = 32.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val backAlpha by animateFloatAsState( + targetValue = if (pagerState.currentPage > 0 || launchSource == LaunchSource.SETTINGS) { + 1f + } else { + 0f + }, + label = "back_button_animation", + ) + OutlinedButton( + modifier = Modifier + .alpha(backAlpha) + .size(56.dp), + shape = RoundedCornerShape(12.dp), + contentPadding = PaddingValues(0.dp), + onClick = { + if (pagerState.currentPage > 0) { + scope.launch { + pagerState.animateScrollToPage( + page = pagerState.currentPage - 1, + animationSpec = onBoardingPageAnimation, + ) + } + } else if (pagerState.currentPage == 0 && launchSource == LaunchSource.SETTINGS) { + processIntent(OnBoardingIntent.Navigate) + } + }, + ) { + Icon( + modifier = Modifier.rotate(180f), + imageVector = Icons.Default.DoubleArrow, + contentDescription = "Next", + ) + } + Spacer(modifier = Modifier.weight(1f)) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + repeat(OnBoardingPage.entries.size) { index -> + val color = if (index == pagerState.currentPage) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.primary.copy(alpha = 0.25f) + } + Box( + modifier = Modifier + .size(8.dp) + .background(color, CircleShape) + ) + } + } + Spacer(modifier = Modifier.weight(1f)) + Button( + modifier = Modifier.size(56.dp), + shape = RoundedCornerShape(12.dp), + contentPadding = PaddingValues(0.dp), + onClick = { + if (pagerState.currentPage == OnBoardingPage.entries.size - 1) { + processIntent(OnBoardingIntent.Navigate) + } else { + scope.launch { + pagerState.animateScrollToPage( + page = pagerState.currentPage + 1, + animationSpec = onBoardingPageAnimation, + ) + } + } + }, + ) { + val icon = + if (pagerState.currentPage == OnBoardingPage.entries.last().ordinal) { + Icons.Default.Check + } else { + Icons.Default.DoubleArrow + } + Icon( + imageVector = icon, + contentDescription = "Next", + ) + } + } + } + }, + containerColor = MaterialTheme.colorScheme.surface, + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = pagerState, + beyondBoundsPageCount = OnBoardingPage.entries.size, + userScrollEnabled = false, + ) { index -> + when (OnBoardingPage.entries[index]) { + OnBoardingPage.Form -> FormPageContent() + OnBoardingPage.Providers -> ProviderPageContent() + OnBoardingPage.LocalDiffusion -> LocalDiffusionPageContent() + OnBoardingPage.LookAndFeel -> LookAndFeelPageContent( + darkThemeToken = state.darkThemeToken, + ) + } + } + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingState.kt new file mode 100644 index 00000000..812a4082 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingState.kt @@ -0,0 +1,8 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding + +import com.shifthackz.aisdv1.domain.entity.DarkThemeToken +import com.shifthackz.android.core.mvi.MviState + +data class OnBoardingState( + val darkThemeToken: DarkThemeToken = DarkThemeToken.FRAPPE, +) : MviState diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingText.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingText.kt new file mode 100644 index 00000000..a40aed1e --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingText.kt @@ -0,0 +1,47 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle + +private const val CHAR_BOLD_START = '[' +private const val CHAR_BOLD_END = ']' + +@Composable +fun buildOnBoardingText(@StringRes resId: Int): AnnotatedString = buildAnnotatedString { + val list = mutableListOf>() + val fullString = stringResource(id = resId) + var currentSequence = "" + var currentFontWeight = FontWeight.Light + for (index in fullString.indices) { + val char = fullString[index] + fun add() { + list.add(currentSequence to currentFontWeight) + currentSequence = "" + } + if (char == CHAR_BOLD_START) { + add() + currentFontWeight = FontWeight.Bold + } else if (char == CHAR_BOLD_END) { + add() + currentFontWeight = FontWeight.Light + } else if (index == fullString.length - 1) { + currentSequence += char + add() + } + if (char == CHAR_BOLD_START || char == CHAR_BOLD_END) { + continue + } + currentSequence += char + } + list.forEach { + withStyle(style = SpanStyle(fontWeight = it.second)) { + append(it.first) + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingViewModel.kt new file mode 100644 index 00000000..8a2f057e --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/OnBoardingViewModel.kt @@ -0,0 +1,49 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding + +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.viewmodel.MviRxViewModel +import com.shifthackz.aisdv1.domain.entity.DarkThemeToken +import com.shifthackz.aisdv1.domain.preference.PreferenceManager +import com.shifthackz.aisdv1.domain.usecase.splash.SplashNavigationUseCase +import com.shifthackz.aisdv1.presentation.model.LaunchSource +import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter +import com.shifthackz.aisdv1.presentation.navigation.router.main.postSplashNavigation +import com.shifthackz.android.core.mvi.EmptyEffect +import io.reactivex.rxjava3.kotlin.subscribeBy + +class OnBoardingViewModel( + val launchSource: LaunchSource, + private val mainRouter: MainRouter, + private val splashNavigationUseCase: SplashNavigationUseCase, + private val preferenceManager: PreferenceManager, + private val schedulersProvider: SchedulersProvider, +) : MviRxViewModel() { + + override val initialState = OnBoardingState() + + init { + updateState { + val token = DarkThemeToken.parse(preferenceManager.designDarkThemeToken) + it.copy(darkThemeToken = token) + } + } + + override fun processIntent(intent: OnBoardingIntent) { + when (intent) { + OnBoardingIntent.Navigate -> { + preferenceManager.onBoardingComplete = true + when (launchSource) { + LaunchSource.SPLASH -> !splashNavigationUseCase() + .subscribeOnMainThread(schedulersProvider) + .subscribeBy(::errorLog) { action -> + mainRouter.postSplashNavigation(action) + } + + LaunchSource.SETTINGS -> mainRouter.navigateBack() + } + } + } + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/FormPageContent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/FormPageContent.kt new file mode 100644 index 00000000..a24cfebd --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/FormPageContent.kt @@ -0,0 +1,65 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding.page + +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import com.shifthackz.aisdv1.core.extensions.gesturesDisabled +import com.shifthackz.aisdv1.presentation.screen.onboarding.buildOnBoardingText +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingDensity +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingPhoneAspectRatio +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingPhoneWidthFraction +import com.shifthackz.aisdv1.presentation.screen.txt2img.TextToImageScreenContent +import com.shifthackz.aisdv1.presentation.screen.txt2img.TextToImageState +import com.shifthackz.aisdv1.presentation.widget.frame.PhoneFrame +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +@Composable +fun FormPageContent( + modifier: Modifier = Modifier, +) = Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, +) { + Spacer(modifier = Modifier.weight(1f)) + Text( + text = buildOnBoardingText(LocalizationR.string.on_boarding_page_form_title), + fontSize = 24.sp, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.weight(1f)) + PhoneFrame( + modifier = Modifier.fillMaxWidth(onBoardingPhoneWidthFraction), + ) { + CompositionLocalProvider(LocalDensity provides onBoardingDensity) { + TextToImageScreenContent( + modifier = Modifier + .gesturesDisabled() + .aspectRatio(onBoardingPhoneAspectRatio), + state = TextToImageState( + onBoardingDemo = true, + advancedToggleButtonVisible = false, + advancedOptionsVisible = true, + formPromptTaggedInput = true, + prompt = "man, photorealistic, black hair, aviator glasses, handsome, beautiful, nature background, , ", + negativePrompt = "bad anatomy, bad fingers, distorted, jpeg artifacts", + selectedSampler = "DPM++ 2M", + availableSamplers = listOf("DPM++ 2M"), + seed = "050598", + subSeed = "151297", + subSeedStrength = 0.69f, + ), + ) + } + } + Spacer(modifier = Modifier.weight(1f)) +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/LocalDiffusionPageContent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/LocalDiffusionPageContent.kt new file mode 100644 index 00000000..9893fe8f --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/LocalDiffusionPageContent.kt @@ -0,0 +1,90 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding.page + +import androidx.compose.foundation.background +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import com.shifthackz.aisdv1.core.extensions.gesturesDisabled +import com.shifthackz.aisdv1.domain.entity.ServerSource +import com.shifthackz.aisdv1.presentation.screen.onboarding.buildOnBoardingText +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingDensity +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingPhoneAspectRatio +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingPhoneWidthFraction +import com.shifthackz.aisdv1.presentation.screen.txt2img.TextToImageScreenContent +import com.shifthackz.aisdv1.presentation.screen.txt2img.TextToImageState +import com.shifthackz.aisdv1.presentation.widget.dialog.GeneratingProgressDialogContent +import com.shifthackz.aisdv1.presentation.widget.frame.PhoneFrame +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +@Composable +fun LocalDiffusionPageContent( + modifier: Modifier = Modifier, +) = Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, +) { + Spacer(modifier = Modifier.weight(1f)) + Text( + text = buildOnBoardingText(LocalizationR.string.on_boarding_page_local_title), + fontSize = 24.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight(450), + ) + Spacer(modifier = Modifier.weight(1f)) + PhoneFrame( + modifier = Modifier.fillMaxWidth(onBoardingPhoneWidthFraction), + ) { + CompositionLocalProvider(LocalDensity provides onBoardingDensity) { + val localModifier = Modifier + .gesturesDisabled() + .aspectRatio(onBoardingPhoneAspectRatio) + Box( + contentAlignment = Alignment.Center, + ) { + TextToImageScreenContent( + modifier = localModifier, + state = TextToImageState( + onBoardingDemo = true, + mode = ServerSource.LOCAL, + advancedToggleButtonVisible = false, + advancedOptionsVisible = true, + formPromptTaggedInput = true, + prompt = "man, photorealistic, black hair, aviator glasses, handsome, beautiful, nature background, , ", + negativePrompt = "bad anatomy, bad fingers, distorted, jpeg artifacts", + seed = "050598", + ), + ) + Box( + modifier = localModifier + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.7f)), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier.fillMaxWidth(0.85f), + contentAlignment = Alignment.Center, + ) { + GeneratingProgressDialogContent( + titleResId = LocalizationR.string.communicating_local_title, + step = 3 to 20, + ) + } + } + } + } + } + Spacer(modifier = Modifier.weight(1f)) +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/LookAndFeelPageContent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/LookAndFeelPageContent.kt new file mode 100644 index 00000000..c7f570e1 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/LookAndFeelPageContent.kt @@ -0,0 +1,102 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding.page + +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import com.shifthackz.aisdv1.core.extensions.gesturesDisabled +import com.shifthackz.aisdv1.domain.entity.ColorToken +import com.shifthackz.aisdv1.domain.entity.DarkThemeToken +import com.shifthackz.aisdv1.presentation.screen.onboarding.buildOnBoardingText +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingDensity +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingPhoneAspectRatio +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingPhoneWidthFraction +import com.shifthackz.aisdv1.presentation.screen.settings.SettingsScreenContent +import com.shifthackz.aisdv1.presentation.screen.settings.SettingsState +import com.shifthackz.aisdv1.presentation.theme.global.AiSdAppTheme +import com.shifthackz.aisdv1.presentation.theme.global.AiSdAppThemeState +import com.shifthackz.aisdv1.presentation.theme.isSdAppInDarkTheme +import com.shifthackz.aisdv1.presentation.widget.frame.PhoneFrame +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +@Composable +fun LookAndFeelPageContent( + modifier: Modifier = Modifier, + darkThemeToken: DarkThemeToken, +) = Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, +) { + val scope = rememberCoroutineScope() + val darkTheme = isSdAppInDarkTheme() + var themeState by remember { + mutableStateOf( + AiSdAppThemeState( + systemColorPalette = false, + systemDarkTheme = false, + darkTheme = darkTheme, + darkThemeToken = darkThemeToken, + ), + ) + } + Spacer(modifier = Modifier.weight(1f)) + Text( + text = buildOnBoardingText(LocalizationR.string.on_boarding_page_ui_title), + fontSize = 24.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight(450), + ) + Spacer(modifier = Modifier.weight(1f)) + PhoneFrame( + modifier = Modifier.fillMaxWidth(onBoardingPhoneWidthFraction), + ) { + CompositionLocalProvider(LocalDensity provides onBoardingDensity) { + AiSdAppTheme(themeState) { + SettingsScreenContent( + modifier = Modifier + .gesturesDisabled() + .aspectRatio(onBoardingPhoneAspectRatio), + state = SettingsState( + loading = false, + onBoardingDemo = true, + colorToken = themeState.colorToken, + darkThemeToken = darkThemeToken, + darkTheme = darkTheme, + ), + ) + } + } + } + DisposableEffect(Unit) { + val job = scope.launch { + while (true) { + delay(700) + themeState = themeState.copy( + colorToken = ColorToken.entries.random(), + ) + } + } + onDispose { + job.cancel() + } + } + Spacer(modifier = Modifier.weight(1f)) +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/ProvidersPageContent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/ProvidersPageContent.kt new file mode 100644 index 00000000..362ccfbd --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/onboarding/page/ProvidersPageContent.kt @@ -0,0 +1,85 @@ +package com.shifthackz.aisdv1.presentation.screen.onboarding.page + +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp +import com.shifthackz.aisdv1.core.extensions.gesturesDisabled +import com.shifthackz.aisdv1.domain.entity.ServerSource +import com.shifthackz.aisdv1.presentation.screen.onboarding.buildOnBoardingText +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingDensity +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingPhoneAspectRatio +import com.shifthackz.aisdv1.presentation.screen.onboarding.onBoardingPhoneWidthFraction +import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupScreenContent +import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupState +import com.shifthackz.aisdv1.presentation.widget.frame.PhoneFrame +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import com.shifthackz.aisdv1.core.localization.R as LocalizationR + +@Composable +fun ProviderPageContent( + modifier: Modifier = Modifier, +) = Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, +) { + val scope = rememberCoroutineScope() + var serverState by remember { + mutableStateOf( + ServerSetupState( + showBackNavArrow = false, + ) + ) + } + Spacer(modifier = Modifier.weight(1f)) + Text( + text = buildOnBoardingText(LocalizationR.string.on_boarding_page_provider_title), + fontSize = 24.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight(450), + ) + Spacer(modifier = Modifier.weight(1f)) + PhoneFrame( + modifier = Modifier.fillMaxWidth(onBoardingPhoneWidthFraction), + ) { + CompositionLocalProvider(LocalDensity provides onBoardingDensity) { + ServerSetupScreenContent( + modifier = Modifier + .gesturesDisabled() + .aspectRatio(onBoardingPhoneAspectRatio), + state = serverState, + ) + } + } + Spacer(modifier = Modifier.weight(1f)) + DisposableEffect(Unit) { + val job = scope.launch { + while (true) { + delay(1200) + serverState = serverState.copy( + mode = ServerSource.entries.random(), + ) + } + } + onDispose { + job.cancel() + } + } +} 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 be85a4fb..41f68563 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 @@ -40,6 +40,8 @@ sealed interface SettingsIntent : MviIntent { } data object Donate : Action + + data object OnBoarding : Action } sealed class LaunchUrl : SettingsIntent, KoinComponent { 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 f4757741..0c13e5d4 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 @@ -20,6 +20,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll 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.Circle import androidx.compose.material.icons.filled.Code @@ -124,7 +125,7 @@ fun SettingsScreen() { }, applySystemUiColors = false, ) { state, intentHandler -> - ScreenContent( + SettingsScreenContent( state = state, processIntent = intentHandler, ) @@ -132,10 +133,10 @@ fun SettingsScreen() { } @Composable -private fun ScreenContent( +fun SettingsScreenContent( modifier: Modifier = Modifier, state: SettingsState, - processIntent: (SettingsIntent) -> Unit, + processIntent: (SettingsIntent) -> Unit = {}, ) { Box(modifier) { Scaffold( @@ -202,8 +203,9 @@ private fun ContentSettingsState( ) { systemUiController.setNavigationBarColor(navBarColor) } + val scrollState = rememberScrollState() Column( - modifier = modifier.verticalScroll(rememberScrollState()), + modifier = modifier.verticalScroll(scrollState), ) { val headerModifier = Modifier.padding(top = 28.dp, bottom = 8.dp) @@ -215,220 +217,228 @@ private fun ContentSettingsState( .fillMaxWidth() .padding(top = 4.dp, start = 4.dp) - //region MAIN SETTINGS - SettingsHeader( - modifier = headerModifier, - loading = state.loading, - text = LocalizationR.string.settings_header_server.asUiText(), - ) - SettingsItem( - modifier = itemModifier, - loading = state.loading, - startIcon = Icons.Default.SettingsEthernet, - text = LocalizationR.string.settings_item_config.asUiText(), - endValueText = when (state.serverSource) { - ServerSource.AUTOMATIC1111 -> LocalizationR.string.srv_type_own_short - ServerSource.HORDE -> LocalizationR.string.srv_type_horde_short - ServerSource.HUGGING_FACE -> LocalizationR.string.srv_type_hugging_face_short - ServerSource.OPEN_AI -> LocalizationR.string.srv_type_open_ai - ServerSource.STABILITY_AI -> LocalizationR.string.srv_type_stability_ai - ServerSource.LOCAL -> LocalizationR.string.srv_type_local_short - ServerSource.SWARM_UI -> LocalizationR.string.srv_type_swarm_ui - }.asUiText(), - onClick = { processIntent(SettingsIntent.NavigateConfiguration) }, - ) - if (state.showStabilityAiCredits) SettingsItem( - modifier = itemModifier, - loading = state.loading, - enabled = false, - startIcon = Icons.Default.Circle, - text = LocalizationR.string.settings_item_stability_ai_credits.asUiText(), - endValueText = state.stabilityAiCredits.roundTo(4).toString().asUiText(), - ) - if (state.showSdModelSelector) SettingsItem( - modifier = itemModifier, - loading = state.loading, - startIcon = Icons.Default.AutoFixNormal, - text = LocalizationR.string.settings_item_sd_model.asUiText(), - endValueText = state.sdModelSelected.asUiText(), - onClick = { processIntent(SettingsIntent.SdModel.OpenChooser) }, - ) - if (state.showLocalUseNNAPI) { + if (!state.onBoardingDemo) { + //region MAIN SETTINGS + SettingsHeader( + modifier = headerModifier, + loading = state.loading, + text = LocalizationR.string.settings_header_server.asUiText(), + ) SettingsItem( modifier = itemModifier, loading = state.loading, - startIcon = Icons.Default.AccountTree, - text = LocalizationR.string.settings_item_local_nnapi.asUiText(), + startIcon = Icons.Default.SettingsEthernet, + text = LocalizationR.string.settings_item_config.asUiText(), + endValueText = when (state.serverSource) { + ServerSource.AUTOMATIC1111 -> LocalizationR.string.srv_type_own_short + ServerSource.HORDE -> LocalizationR.string.srv_type_horde_short + ServerSource.HUGGING_FACE -> LocalizationR.string.srv_type_hugging_face_short + ServerSource.OPEN_AI -> LocalizationR.string.srv_type_open_ai + ServerSource.STABILITY_AI -> LocalizationR.string.srv_type_stability_ai + ServerSource.LOCAL -> LocalizationR.string.srv_type_local_short + ServerSource.SWARM_UI -> LocalizationR.string.srv_type_swarm_ui + }.asUiText(), + onClick = { processIntent(SettingsIntent.NavigateConfiguration) }, + ) + if (state.showStabilityAiCredits) SettingsItem( + modifier = itemModifier, + loading = state.loading, + enabled = false, + startIcon = Icons.Default.Circle, + text = LocalizationR.string.settings_item_stability_ai_credits.asUiText(), + endValueText = state.stabilityAiCredits.roundTo(4).toString().asUiText(), + ) + if (state.showSdModelSelector) SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.AutoFixNormal, + text = LocalizationR.string.settings_item_sd_model.asUiText(), endValueText = state.sdModelSelected.asUiText(), - onClick = { processIntent(SettingsIntent.UpdateFlag.NNAPI(!state.localUseNNAPI)) }, + onClick = { processIntent(SettingsIntent.SdModel.OpenChooser) }, + ) + if (state.showLocalUseNNAPI) { + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.AccountTree, + text = LocalizationR.string.settings_item_local_nnapi.asUiText(), + endValueText = state.sdModelSelected.asUiText(), + onClick = { processIntent(SettingsIntent.UpdateFlag.NNAPI(!state.localUseNNAPI)) }, + endValueContent = { + Switch( + modifier = Modifier.padding(horizontal = 8.dp), + checked = state.localUseNNAPI, + onCheckedChange = { processIntent(SettingsIntent.UpdateFlag.NNAPI(it)) }, + ) + } + ) + AnimatedVisibility(visible = !state.loading) { + Text( + modifier = warningModifier, + text = stringResource(id = LocalizationR.string.settings_item_local_nnapi_warning), + style = MaterialTheme.typography.labelMedium, + ) + } + } + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.MiscellaneousServices, + text = LocalizationR.string.settings_item_background_generation.asUiText(), + onClick = { + processIntent(SettingsIntent.UpdateFlag.BackgroundGeneration(!state.backgroundGeneration)) + }, endValueContent = { Switch( modifier = Modifier.padding(horizontal = 8.dp), - checked = state.localUseNNAPI, - onCheckedChange = { processIntent(SettingsIntent.UpdateFlag.NNAPI(it)) }, + checked = state.backgroundGeneration, + onCheckedChange = { + processIntent(SettingsIntent.UpdateFlag.BackgroundGeneration(it)) + }, ) - } + }, ) AnimatedVisibility(visible = !state.loading) { Text( modifier = warningModifier, - text = stringResource(id = LocalizationR.string.settings_item_local_nnapi_warning), + text = stringResource(id = LocalizationR.string.settings_item_background_generation_warning), style = MaterialTheme.typography.labelMedium, ) } - } - SettingsItem( - modifier = itemModifier, - loading = state.loading, - startIcon = Icons.Default.MiscellaneousServices, - text = LocalizationR.string.settings_item_background_generation.asUiText(), - onClick = { - processIntent(SettingsIntent.UpdateFlag.BackgroundGeneration(!state.backgroundGeneration)) - }, - endValueContent = { - Switch( - modifier = Modifier.padding(horizontal = 8.dp), - checked = state.backgroundGeneration, - onCheckedChange = { - processIntent(SettingsIntent.UpdateFlag.BackgroundGeneration(it)) - }, + AnimatedVisibility(visible = !state.loading && state.developerMode) { + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.DeveloperMode, + text = LocalizationR.string.title_debug_menu.asUiText(), + onClick = { processIntent(SettingsIntent.NavigateDeveloperMode) }, ) - }, - ) - AnimatedVisibility(visible = !state.loading) { - Text( - modifier = warningModifier, - text = stringResource(id = LocalizationR.string.settings_item_background_generation_warning), - style = MaterialTheme.typography.labelMedium, + } + //endregion + + //region APP SETTINGS + SettingsHeader( + modifier = headerModifier, + loading = state.loading, + text = LocalizationR.string.settings_header_app.asUiText(), + ) + if (state.showMonitorConnectionOption) SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.Refresh, + text = LocalizationR.string.settings_item_monitor_connection.asUiText(), + onClick = { + processIntent(SettingsIntent.UpdateFlag.MonitorConnection(!state.monitorConnectivity)) + }, + endValueContent = { + Switch( + modifier = Modifier.padding(horizontal = 8.dp), + checked = state.monitorConnectivity, + onCheckedChange = { + processIntent( + SettingsIntent.UpdateFlag.MonitorConnection( + it + ) + ) + }, + ) + }, ) - } - AnimatedVisibility(visible = !state.loading && state.developerMode) { SettingsItem( modifier = itemModifier, loading = state.loading, - startIcon = Icons.Default.DeveloperMode, - text = LocalizationR.string.title_debug_menu.asUiText(), - onClick = { processIntent(SettingsIntent.NavigateDeveloperMode) }, + startIcon = Icons.Default.Save, + text = UiText.Concat( + LocalizationR.string.settings_item_auto_save.asUiText(), + if (state.backgroundGeneration) "*" else "", + ), + onClick = { + processIntent(SettingsIntent.UpdateFlag.AutoSaveResult(!state.autoSaveAiResults)) + }, + endValueContent = { + Switch( + modifier = Modifier.padding(horizontal = 8.dp), + checked = state.autoSaveAiResults, + onCheckedChange = { + processIntent(SettingsIntent.UpdateFlag.AutoSaveResult(it)) + }, + ) + }, ) - } - //endregion - - //region APP SETTINGS - SettingsHeader( - modifier = headerModifier, - loading = state.loading, - text = LocalizationR.string.settings_header_app.asUiText(), - ) - if (state.showMonitorConnectionOption) SettingsItem( - modifier = itemModifier, - loading = state.loading, - startIcon = Icons.Default.Refresh, - text = LocalizationR.string.settings_item_monitor_connection.asUiText(), - onClick = { - processIntent(SettingsIntent.UpdateFlag.MonitorConnection(!state.monitorConnectivity)) - }, - endValueContent = { - Switch( - modifier = Modifier.padding(horizontal = 8.dp), - checked = state.monitorConnectivity, - onCheckedChange = { processIntent(SettingsIntent.UpdateFlag.MonitorConnection(it)) }, - ) - }, - ) - SettingsItem( - modifier = itemModifier, - loading = state.loading, - startIcon = Icons.Default.Save, - text = UiText.Concat( - LocalizationR.string.settings_item_auto_save.asUiText(), - if (state.backgroundGeneration) "*" else "", - ), - onClick = { - processIntent(SettingsIntent.UpdateFlag.AutoSaveResult(!state.autoSaveAiResults)) - }, - endValueContent = { - Switch( - modifier = Modifier.padding(horizontal = 8.dp), - checked = state.autoSaveAiResults, - onCheckedChange = { - processIntent(SettingsIntent.UpdateFlag.AutoSaveResult(it)) - }, + AnimatedVisibility( + visible = !state.loading && state.backgroundGeneration, + ) { + Text( + modifier = warningModifier, + text = stringResource(id = LocalizationR.string.settings_item_auto_save_warning), + style = MaterialTheme.typography.labelMedium, ) - }, - ) - AnimatedVisibility( - visible = !state.loading && state.backgroundGeneration, - ) { - Text( - modifier = warningModifier, - text = stringResource(id = LocalizationR.string.settings_item_auto_save_warning), - style = MaterialTheme.typography.labelMedium, + } + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.Folder, + text = LocalizationR.string.settings_item_auto_save_media_store.asUiText(), + onClick = { + processIntent(SettingsIntent.UpdateFlag.SaveToMediaStore(!state.saveToMediaStore)) + }, + endValueContent = { + Switch( + modifier = Modifier.padding(horizontal = 8.dp), + checked = state.saveToMediaStore, + onCheckedChange = { + processIntent(SettingsIntent.UpdateFlag.SaveToMediaStore(it)) + }, + ) + }, ) - } - SettingsItem( - modifier = itemModifier, - loading = state.loading, - startIcon = Icons.Default.Folder, - text = LocalizationR.string.settings_item_auto_save_media_store.asUiText(), - onClick = { - processIntent(SettingsIntent.UpdateFlag.SaveToMediaStore(!state.saveToMediaStore)) - }, - endValueContent = { - Switch( - modifier = Modifier.padding(horizontal = 8.dp), - checked = state.saveToMediaStore, - onCheckedChange = { - processIntent(SettingsIntent.UpdateFlag.SaveToMediaStore(it)) - }, - ) - }, - ) - SettingsItem( - modifier = itemModifier, - loading = state.loading, - startIcon = Icons.Default.Tag, - text = LocalizationR.string.settings_item_tagged_input.asUiText(), - onClick = { - processIntent(SettingsIntent.UpdateFlag.TaggedInput(!state.formPromptTaggedInput)) - }, - endValueContent = { - Switch( - modifier = Modifier.padding(horizontal = 8.dp), - checked = state.formPromptTaggedInput, - onCheckedChange = { - processIntent(SettingsIntent.UpdateFlag.TaggedInput(it)) - }, - ) - }, - ) - if (state.showFormAdvancedOption) { SettingsItem( modifier = itemModifier, loading = state.loading, - startIcon = Icons.Default.DynamicForm, - text = LocalizationR.string.settings_item_advanced_form_default.asUiText(), + startIcon = Icons.Default.Tag, + text = LocalizationR.string.settings_item_tagged_input.asUiText(), onClick = { - processIntent(SettingsIntent.UpdateFlag.AdvancedFormVisibility(!state.formAdvancedOptionsAlwaysShow)) + processIntent(SettingsIntent.UpdateFlag.TaggedInput(!state.formPromptTaggedInput)) }, endValueContent = { Switch( modifier = Modifier.padding(horizontal = 8.dp), - checked = state.formAdvancedOptionsAlwaysShow, + checked = state.formPromptTaggedInput, onCheckedChange = { - processIntent(SettingsIntent.UpdateFlag.AdvancedFormVisibility(it)) + processIntent(SettingsIntent.UpdateFlag.TaggedInput(it)) }, ) }, ) + if (state.showFormAdvancedOption) { + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.DynamicForm, + text = LocalizationR.string.settings_item_advanced_form_default.asUiText(), + onClick = { + processIntent(SettingsIntent.UpdateFlag.AdvancedFormVisibility(!state.formAdvancedOptionsAlwaysShow)) + }, + endValueContent = { + Switch( + modifier = Modifier.padding(horizontal = 8.dp), + checked = state.formAdvancedOptionsAlwaysShow, + onCheckedChange = { + processIntent(SettingsIntent.UpdateFlag.AdvancedFormVisibility(it)) + }, + ) + }, + ) + } + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.DeleteForever, + text = LocalizationR.string.settings_item_clear_cache.asUiText(), + onClick = { processIntent(SettingsIntent.Action.ClearAppCache.Request) }, + ) + //endregion } - SettingsItem( - modifier = itemModifier, - loading = state.loading, - startIcon = Icons.Default.DeleteForever, - text = LocalizationR.string.settings_item_clear_cache.asUiText(), - onClick = { processIntent(SettingsIntent.Action.ClearAppCache.Request)}, - ) - //endregion //region LOOK AND FEEL SettingsHeader( @@ -580,6 +590,13 @@ private fun ContentSettingsState( text = LocalizationR.string.settings_item_donate.asUiText(), onClick = { processIntent(SettingsIntent.Action.Donate) }, ) + SettingsItem( + modifier = itemModifier, + loading = state.loading, + startIcon = Icons.Default.AllInclusive, + text = LocalizationR.string.settings_item_on_boarding.asUiText(), + onClick = { processIntent(SettingsIntent.Action.OnBoarding) }, + ) SettingsItem( modifier = itemModifier, loading = state.loading, diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsState.kt index 432ec831..aff0ad43 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsState.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsState.kt @@ -11,6 +11,7 @@ import com.shifthackz.android.core.mvi.MviState @Immutable data class SettingsState( val loading: Boolean = true, + val onBoardingDemo: Boolean = false, val screenModal: Modal = Modal.None, val serverSource: ServerSource = ServerSource.AUTOMATIC1111, val sdModels: List = emptyList(), 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 ff9c9cd7..a300bde8 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 @@ -16,12 +16,12 @@ import com.shifthackz.aisdv1.domain.usecase.caching.ClearAppCacheUseCase import com.shifthackz.aisdv1.domain.usecase.sdmodel.GetStableDiffusionModelsUseCase import com.shifthackz.aisdv1.domain.usecase.sdmodel.SelectStableDiffusionModelUseCase import com.shifthackz.aisdv1.domain.usecase.stabilityai.ObserveStabilityAiCreditsUseCase +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.navigation.router.drawer.DrawerRouter import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuAccessor import com.shifthackz.aisdv1.presentation.screen.drawer.DrawerIntent -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.kotlin.subscribeBy import com.shifthackz.aisdv1.core.localization.R as LocalizationR @@ -109,7 +109,7 @@ class SettingsViewModel( } SettingsIntent.NavigateConfiguration -> mainRouter.navigateToServerSetup( - ServerSetupLaunchSource.SETTINGS + LaunchSource.SETTINGS ) SettingsIntent.NavigateDeveloperMode -> mainRouter.navigateToDebugMenu() @@ -210,6 +210,10 @@ class SettingsViewModel( SettingsIntent.Action.Donate -> mainRouter.navigateToDonate() + SettingsIntent.Action.OnBoarding -> mainRouter.navigateToOnBoarding( + source = LaunchSource.SETTINGS, + ) + is SettingsIntent.UpdateFlag.BackgroundGeneration -> { if (intent.flag) { emitEffect(SettingsEffect.RequestPermission.Notifications) 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 ddba12fe..a83405f0 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 @@ -86,7 +86,7 @@ fun ServerSetupScreen( } }, ) { state, intentHandler -> - ScreenContent( + ServerSetupScreenContent( modifier = modifier.fillMaxSize(), state = state, buildInfoProvider = buildInfoProvider, @@ -96,7 +96,7 @@ fun ServerSetupScreen( } @Composable -private fun ScreenContent( +fun ServerSetupScreenContent( modifier: Modifier = Modifier, state: ServerSetupState, buildInfoProvider: BuildInfoProvider = BuildInfoProvider.stub, diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupState.kt index 7e91bbf6..553e7bde 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupState.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupState.kt @@ -87,15 +87,6 @@ data class ServerSetupState( ) } -enum class ServerSetupLaunchSource { - SPLASH, - SETTINGS; - - companion object { - fun fromKey(key: Int) = entries.firstOrNull { it.ordinal == key } ?: SPLASH - } -} - val Configuration.authType: ServerSetupState.AuthType get() { val noCredentials = ServerSetupState.AuthType.ANONYMOUS diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModel.kt index d02f1287..d963e80f 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModel.kt @@ -21,6 +21,7 @@ import com.shifthackz.aisdv1.domain.usecase.downloadable.DownloadModelUseCase import com.shifthackz.aisdv1.domain.usecase.downloadable.GetLocalAiModelsUseCase import com.shifthackz.aisdv1.domain.usecase.huggingface.FetchAndGetHuggingFaceModelsUseCase import com.shifthackz.aisdv1.domain.usecase.settings.GetConfigurationUseCase +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter import com.shifthackz.aisdv1.presentation.screen.setup.mappers.mapLocalCustomModelSwitchState @@ -32,7 +33,7 @@ import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.kotlin.subscribeBy class ServerSetupViewModel( - launchSource: ServerSetupLaunchSource, + launchSource: LaunchSource, getConfigurationUseCase: GetConfigurationUseCase, getLocalAiModelsUseCase: GetLocalAiModelsUseCase, fetchAndGetHuggingFaceModelsUseCase: FetchAndGetHuggingFaceModelsUseCase, @@ -49,7 +50,7 @@ class ServerSetupViewModel( ) : MviRxViewModel() { override val initialState = ServerSetupState( - showBackNavArrow = launchSource == ServerSetupLaunchSource.SETTINGS, + showBackNavArrow = launchSource == LaunchSource.SETTINGS, ) private val credentials: AuthorizationCredentials diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/splash/SplashViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/splash/SplashViewModel.kt index a2aa431d..ee148e3f 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/splash/SplashViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/splash/SplashViewModel.kt @@ -6,7 +6,7 @@ import com.shifthackz.aisdv1.core.common.schedulers.subscribeOnMainThread import com.shifthackz.aisdv1.core.viewmodel.MviRxViewModel import com.shifthackz.aisdv1.domain.usecase.splash.SplashNavigationUseCase import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource +import com.shifthackz.aisdv1.presentation.navigation.router.main.postSplashNavigation import com.shifthackz.android.core.mvi.EmptyEffect import com.shifthackz.android.core.mvi.EmptyIntent import com.shifthackz.android.core.mvi.EmptyState @@ -23,14 +23,6 @@ class SplashViewModel( init { !splashNavigationUseCase() .subscribeOnMainThread(schedulersProvider) - .subscribeBy(::errorLog) { action -> - when (action) { - SplashNavigationUseCase.Action.LAUNCH_ONBOARDING -> {} - SplashNavigationUseCase.Action.LAUNCH_SERVER_SETUP -> mainRouter.navigateToServerSetup( - source = ServerSetupLaunchSource.SPLASH - ) - SplashNavigationUseCase.Action.LAUNCH_HOME -> mainRouter.navigateToPostSplashConfigLoader() - } - } + .subscribeBy(::errorLog) { action -> mainRouter.postSplashNavigation(action) } } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageScreen.kt index c3563be3..af121398 100755 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageScreen.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageScreen.kt @@ -52,7 +52,7 @@ fun TextToImageScreen() { viewModel = koinViewModel(), applySystemUiColors = false, ) { state, intentHandler -> - ScreenContent( + TextToImageScreenContent( modifier = Modifier.fillMaxSize(), state = state, processIntent = intentHandler, @@ -61,7 +61,7 @@ fun TextToImageScreen() { } @Composable -private fun ScreenContent( +fun TextToImageScreenContent( modifier: Modifier = Modifier, state: TextToImageState, processIntent: (GenerationMviIntent) -> Unit = {}, @@ -185,7 +185,7 @@ private fun ScreenContent( @Composable @Preview(showSystemUi = true, showBackground = true) fun PreviewStateContent() { - ScreenContent( + TextToImageScreenContent( modifier = Modifier.fillMaxSize(), state = TextToImageState( prompt = "Opel Astra H OPC", diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageState.kt index 13b4b996..2dad0725 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageState.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageState.kt @@ -19,6 +19,7 @@ import com.shifthackz.aisdv1.core.localization.R as LocalizationR @Immutable data class TextToImageState( + override val onBoardingDemo: Boolean = false, override val screenModal: Modal = Modal.None, override val mode: ServerSource = ServerSource.AUTOMATIC1111, override val advancedToggleButtonVisible: Boolean = true, @@ -50,6 +51,7 @@ data class TextToImageState( ) : GenerationMviState() { override fun copyState( + onBoardingDemo: Boolean, screenModal: Modal, mode: ServerSource, advancedToggleButtonVisible: Boolean, @@ -79,6 +81,7 @@ data class TextToImageState( batchCount: Int, generateButtonEnabled: Boolean ): GenerationMviState = copy( + onBoardingDemo = onBoardingDemo, screenModal = screenModal, mode = mode, advancedToggleButtonVisible = advancedToggleButtonVisible, diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/theme/global/AiSdAppTheme.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/theme/global/AiSdAppTheme.kt index efa903c2..cb0eee09 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/theme/global/AiSdAppTheme.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/theme/global/AiSdAppTheme.kt @@ -20,30 +20,38 @@ fun AiSdAppTheme( viewModel = koinViewModel(), applySystemUiColors = false, ) { state, _ -> - val context = LocalContext.current - val isDark = if (state.systemDarkTheme) { - isSystemInDarkTheme() - } else { - state.darkTheme - } - if (state.systemColorPalette && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MaterialTheme( - colorScheme = if (isDark) { - dynamicDarkColorScheme(context) - } else { - dynamicLightColorScheme(context) - }, - content = content, - ) - } else { - CatppuccinTheme.Palette( - palette = colorTokenPalette( - token = state.colorToken, - darkThemeToken = state.darkThemeToken, - isDark = isDark - ), - content = content, - ) - } + AiSdAppTheme(state, content) + } +} + +@Composable +fun AiSdAppTheme( + state: AiSdAppThemeState, + content: @Composable () -> Unit, +) { + val context = LocalContext.current + val isDark = if (state.systemDarkTheme) { + isSystemInDarkTheme() + } else { + state.darkTheme + } + if (state.systemColorPalette && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MaterialTheme( + colorScheme = if (isDark) { + dynamicDarkColorScheme(context) + } else { + dynamicLightColorScheme(context) + }, + content = content, + ) + } else { + CatppuccinTheme.Palette( + palette = colorTokenPalette( + token = state.colorToken, + darkThemeToken = state.darkThemeToken, + isDark = isDark, + ), + content = content, + ) } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/Constants.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/Constants.kt index d83360a1..6562ec2f 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/Constants.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/utils/Constants.kt @@ -23,6 +23,8 @@ object Constants { const val ROUTE_LOGGER = "logger" const val ROUTE_IN_PAINT = "in_paint" const val ROUTE_DONATE = "donate" + const val ROUTE_ONBOARDING = "onboarding" + const val ROUTE_ONBOARDING_FULL = "$ROUTE_ONBOARDING/{$PARAM_SOURCE}" const val SUB_SEED_STRENGTH_MIN = 0f const val SUB_SEED_STRENGTH_MAX = 1f diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/dialog/ProgressDialog.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/dialog/ProgressDialog.kt index 31c1787e..1820409a 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/dialog/ProgressDialog.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/dialog/ProgressDialog.kt @@ -44,37 +44,14 @@ fun ProgressDialog( dismissOnBackPress = canDismiss, ), ) { - Surface( - shape = RoundedCornerShape(16.dp), - color = AlertDialogDefaults.containerColor, - ) { - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)) { - Text( - text = stringResource(id = titleResId), - style = TextStyle(fontSize = 16.sp), - fontWeight = FontWeight.Bold, - color = AlertDialogDefaults.titleContentColor, - ) - Text( - modifier = Modifier.padding(top = 14.dp), - text = stringResource(id = subTitleResId), - style = TextStyle(fontSize = 14.sp), - color = AlertDialogDefaults.textContentColor, - ) - ProgressDialogStatus( - waitTimeSeconds = waitTimeSeconds, - positionInQueue = positionInQueue, - step = step, - ) - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - color = AlertDialogDefaults.iconContentColor, - ) - content?.invoke() - } - } + GeneratingProgressDialogContent( + titleResId = titleResId, + subTitleResId = subTitleResId, + waitTimeSeconds = waitTimeSeconds, + positionInQueue = positionInQueue, + step = step, + content = content, + ) } } @@ -128,6 +105,48 @@ fun ProgressDialogCancelButton(onClick: () -> Unit) { } } +@Composable +fun GeneratingProgressDialogContent( + @StringRes titleResId: Int = LocalizationR.string.communicating_progress_title, + @StringRes subTitleResId: Int = LocalizationR.string.communicating_progress_sub_title, + waitTimeSeconds: Int? = null, + positionInQueue: Int? = null, + step: Pair? = null, + content: (@Composable () -> Unit)? = null, +) { + Surface( + shape = RoundedCornerShape(16.dp), + color = AlertDialogDefaults.containerColor, + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)) { + Text( + text = stringResource(id = titleResId), + style = TextStyle(fontSize = 16.sp), + fontWeight = FontWeight.Bold, + color = AlertDialogDefaults.titleContentColor, + ) + Text( + modifier = Modifier.padding(top = 14.dp), + text = stringResource(id = subTitleResId), + style = TextStyle(fontSize = 14.sp), + color = AlertDialogDefaults.textContentColor, + ) + ProgressDialogStatus( + waitTimeSeconds = waitTimeSeconds, + positionInQueue = positionInQueue, + step = step, + ) + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + color = AlertDialogDefaults.iconContentColor, + ) + content?.invoke() + } + } +} + @Composable @Preview(showBackground = true) private fun CommunicationProgressDialogPreview() { diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/frame/PhoneFrame.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/frame/PhoneFrame.kt new file mode 100644 index 00000000..b30b8117 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/frame/PhoneFrame.kt @@ -0,0 +1,99 @@ +package com.shifthackz.aisdv1.presentation.widget.frame + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.presentation.R + +@Composable +fun PhoneFrame( + modifier: Modifier = Modifier, + content: @Composable () -> Unit = {}, +) { + Column( + modifier = modifier + .background(Color.Black, RoundedCornerShape(24.dp)) + .border( + border = BorderStroke(6.dp, Color.Black), + shape = RoundedCornerShape(24.dp) + ) + .clip(RoundedCornerShape(24.dp)), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(18.dp) + .background( + color = Color.Black, + shape = RoundedCornerShape( + topStart = 24.dp, + topEnd = 24.dp, + ), + ), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .alpha(0.5f) + .padding(top = 8.dp) + .width(54.dp) + .height(8.dp) + .clip(RoundedCornerShape(12.dp)), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.fillMaxSize(), + painter = painterResource(id = R.drawable.ic_speaker_texture), + contentDescription = null, + contentScale = ContentScale.Crop, + ) + } + } + Box( + modifier = Modifier, + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .border( + border = BorderStroke(6.dp, Color.Black), + shape = RoundedCornerShape(12.dp) + ) + .padding(6.dp), + ) { + content() + } + } + Box( + modifier = Modifier + .fillMaxWidth() + .height(12.dp) + .background( + color = Color.Black, + shape = RoundedCornerShape( + bottomStart = 24.dp, + bottomEnd = 24.dp, + ), + ), + ) + } + +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/input/GenerationInputForm.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/input/GenerationInputForm.kt index 5e74957b..1a8583d5 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/input/GenerationInputForm.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/input/GenerationInputForm.kt @@ -142,24 +142,28 @@ fun GenerationInputForm( } Column(modifier = modifier) { - when (state.mode) { - ServerSource.AUTOMATIC1111, - ServerSource.SWARM_UI, - ServerSource.STABILITY_AI, - ServerSource.HUGGING_FACE, - ServerSource.LOCAL -> EngineSelectionComponent( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - ) - ServerSource.OPEN_AI -> DropdownTextField( - modifier = Modifier.padding(top = 8.dp), - label = LocalizationR.string.hint_model_open_ai.asUiText(), - value = state.openAiModel, - items = OpenAiModel.entries, - onItemSelected = { processIntent(GenerationMviIntent.Update.OpenAi.Model(it)) }, - ) - else -> Unit + if (!state.onBoardingDemo) { + when (state.mode) { + ServerSource.AUTOMATIC1111, + ServerSource.SWARM_UI, + ServerSource.STABILITY_AI, + ServerSource.HUGGING_FACE, + ServerSource.LOCAL -> EngineSelectionComponent( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + ) + + ServerSource.OPEN_AI -> DropdownTextField( + modifier = Modifier.padding(top = 8.dp), + label = LocalizationR.string.hint_model_open_ai.asUiText(), + value = state.openAiModel, + items = OpenAiModel.entries, + onItemSelected = { processIntent(GenerationMviIntent.Update.OpenAi.Model(it)) }, + ) + + else -> Unit + } } if (state.formPromptTaggedInput) { ChipTextFieldWithItem( diff --git a/presentation/src/main/res/drawable/ic_speaker_texture.jpg b/presentation/src/main/res/drawable/ic_speaker_texture.jpg new file mode 100644 index 00000000..8628231e Binary files /dev/null and b/presentation/src/main/res/drawable/ic_speaker_texture.jpg differ diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImplTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImplTest.kt index ecec9c5c..bbc45def 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImplTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouterImplTest.kt @@ -1,25 +1,13 @@ package com.shifthackz.aisdv1.presentation.navigation.router.main -import com.shifthackz.aisdv1.domain.preference.PreferenceManager +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.navigation.NavigationEffect -import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuAccessor -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.utils.Constants -import io.mockk.mockk -import org.junit.Before import org.junit.Test class MainRouterImplTest { - private val stubPreferenceManager = mockk() - private val stubDebugMenuAccessor = DebugMenuAccessor(stubPreferenceManager) - - private val router = MainRouterImpl(stubDebugMenuAccessor) - - @Before - fun initialize() { - - } + private val router = MainRouterImpl() @Test fun `given user navigates back, expected router emits Back event`() { @@ -51,7 +39,11 @@ class MainRouterImplTest { .test() .also { router.navigateToHomeScreen() } .assertNoErrors() - .assertValueAt(0, NavigationEffect.Navigate.RoutePopUp(Constants.ROUTE_HOME)) + .assertValueAt(0) { actual -> + val expectedRoute = Constants.ROUTE_HOME + actual is NavigationEffect.Navigate.RouteBuilder + && actual.route == expectedRoute + } } @Test @@ -59,11 +51,11 @@ class MainRouterImplTest { router .observe() .test() - .also { router.navigateToServerSetup(ServerSetupLaunchSource.SPLASH) } + .also { router.navigateToServerSetup(LaunchSource.SPLASH) } .assertNoErrors() .assertValueAt(0) { actual -> val expectedRoute = - "${Constants.ROUTE_SERVER_SETUP}/${ServerSetupLaunchSource.SPLASH.ordinal}" + "${Constants.ROUTE_SERVER_SETUP}/${LaunchSource.SPLASH.ordinal}" actual is NavigationEffect.Navigate.RouteBuilder && actual.route == expectedRoute } @@ -74,11 +66,11 @@ class MainRouterImplTest { router .observe() .test() - .also { router.navigateToServerSetup(ServerSetupLaunchSource.SETTINGS) } + .also { router.navigateToServerSetup(LaunchSource.SETTINGS) } .assertNoErrors() .assertValueAt(0) { actual -> val expectedRoute = - "${Constants.ROUTE_SERVER_SETUP}/${ServerSetupLaunchSource.SETTINGS.ordinal}" + "${Constants.ROUTE_SERVER_SETUP}/${LaunchSource.SETTINGS.ordinal}" actual is NavigationEffect.Navigate.RouteBuilder && actual.route == expectedRoute } diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailViewModelTest.kt index e3431228..3d0efcb6 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailViewModelTest.kt @@ -81,30 +81,28 @@ class GalleryDetailViewModelTest : CoreViewModelTest() { @Test fun `initialized, loaded ai generation result, expected UI state is Content`() { - runTest { - val expected = GalleryDetailState.Content( - tabs = GalleryDetailState.Tab.consume(mockAiGenerationResult.type), - generationType = mockAiGenerationResult.type, - id = mockAiGenerationResult.id, - bitmap = stubBitmap, - inputBitmap = stubBitmap, - createdAt = mockAiGenerationResult.createdAt.toString().asUiText(), - type = mockAiGenerationResult.type.key.asUiText(), - prompt = mockAiGenerationResult.prompt.asUiText(), - negativePrompt = mockAiGenerationResult.negativePrompt.asUiText(), - size = "512 X 512".asUiText(), - samplingSteps = mockAiGenerationResult.samplingSteps.toString().asUiText(), - cfgScale = mockAiGenerationResult.cfgScale.toString().asUiText(), - restoreFaces = mockAiGenerationResult.restoreFaces.mapToUi(), - sampler = mockAiGenerationResult.sampler.asUiText(), - seed = mockAiGenerationResult.seed.asUiText(), - subSeed = mockAiGenerationResult.subSeed.asUiText(), - subSeedStrength = mockAiGenerationResult.subSeedStrength.toString().asUiText(), - denoisingStrength = mockAiGenerationResult.denoisingStrength.toString().asUiText(), - ) - val actual = viewModel.state.value - Assert.assertEquals(expected, actual) - } + val expected = GalleryDetailState.Content( + tabs = GalleryDetailState.Tab.consume(mockAiGenerationResult.type), + generationType = mockAiGenerationResult.type, + id = mockAiGenerationResult.id, + bitmap = stubBitmap, + inputBitmap = stubBitmap, + createdAt = mockAiGenerationResult.createdAt.toString().asUiText(), + type = mockAiGenerationResult.type.key.asUiText(), + prompt = mockAiGenerationResult.prompt.asUiText(), + negativePrompt = mockAiGenerationResult.negativePrompt.asUiText(), + size = "512 X 512".asUiText(), + samplingSteps = mockAiGenerationResult.samplingSteps.toString().asUiText(), + cfgScale = mockAiGenerationResult.cfgScale.toString().asUiText(), + restoreFaces = mockAiGenerationResult.restoreFaces.mapToUi(), + sampler = mockAiGenerationResult.sampler.asUiText(), + seed = mockAiGenerationResult.seed.asUiText(), + subSeed = mockAiGenerationResult.subSeed.asUiText(), + subSeedStrength = mockAiGenerationResult.subSeedStrength.toString().asUiText(), + denoisingStrength = mockAiGenerationResult.denoisingStrength.toString().asUiText(), + ) + val actual = viewModel.state.value + Assert.assertEquals(expected, actual) } @Test diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/img2img/ImageToImageViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/img2img/ImageToImageViewModelTest.kt index 50e45fbd..e2982572 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/img2img/ImageToImageViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/img2img/ImageToImageViewModelTest.kt @@ -19,10 +19,10 @@ import com.shifthackz.aisdv1.presentation.core.GenerationFormUpdateEvent import com.shifthackz.aisdv1.presentation.core.GenerationMviIntent import com.shifthackz.aisdv1.presentation.mocks.mockAiGenerationResult import com.shifthackz.aisdv1.presentation.model.InPaintModel +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.screen.drawer.DrawerIntent import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintStateProducer -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import io.mockk.every import io.mockk.mockk import io.mockk.unmockkAll @@ -446,7 +446,7 @@ class ImageToImageViewModelTest : CoreGenerationMviViewModelTest() { + + private var source = LaunchSource.SPLASH + + private val stubMainRouter = mockk() + private val stubSplashNavigationUseCase = mockk() + private val stubPreferenceManager = mockk() + + override val testViewModelStrategy = CoreViewModelInitializeStrategy.InitializeEveryTime + + override fun initializeViewModel() = OnBoardingViewModel( + source, + stubMainRouter, + stubSplashNavigationUseCase, + stubPreferenceManager, + stubSchedulersProvider, + ) + + @Before + override fun initialize() { + super.initialize() + + every { + stubPreferenceManager::designDarkThemeToken.get() + } returns DarkThemeToken.FRAPPE.toString() + + every { + stubPreferenceManager::onBoardingComplete.set(any()) + } returns Unit + + every { + stubPreferenceManager::onBoardingComplete.get() + } returns true + } + + @Test + fun `given received Navigate intent, expected onBoardingComplete updated in preference, navigation processed`() { + source = LaunchSource.SPLASH + + every { + stubSplashNavigationUseCase() + } returns Single.just(SplashNavigationUseCase.Action.LAUNCH_HOME) + + viewModel.processIntent(OnBoardingIntent.Navigate) + + verify { + stubMainRouter.navigateToPostSplashConfigLoader() + } + } + + @Test + fun `given received Navigate intent, expected onBoardingComplete updated in preference, navigateBack processed`() { + source = LaunchSource.SETTINGS + + every { + stubMainRouter.navigateBack() + } returns Unit + + viewModel.processIntent(OnBoardingIntent.Navigate) + + verify { + stubMainRouter.navigateBack() + } + } +} diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModelTest.kt index 5aa56c54..00f18879 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/settings/SettingsViewModelTest.kt @@ -13,11 +13,11 @@ import com.shifthackz.aisdv1.domain.usecase.sdmodel.SelectStableDiffusionModelUs import com.shifthackz.aisdv1.domain.usecase.stabilityai.ObserveStabilityAiCreditsUseCase import com.shifthackz.aisdv1.presentation.core.CoreViewModelTest import com.shifthackz.aisdv1.presentation.mocks.mockStableDiffusionModels +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.navigation.router.drawer.DrawerRouter import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuAccessor -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.stub.stubSchedulersProvider import io.mockk.every import io.mockk.mockk @@ -161,7 +161,7 @@ class SettingsViewModelTest : CoreViewModelTest() { viewModel.processIntent(SettingsIntent.NavigateConfiguration) verify { - stubMainRouter.navigateToServerSetup(ServerSetupLaunchSource.SETTINGS) + stubMainRouter.navigateToServerSetup(LaunchSource.SETTINGS) } } diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModelTest.kt index 178d0c6a..5c23b4e6 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupViewModelTest.kt @@ -18,6 +18,7 @@ import com.shifthackz.aisdv1.presentation.core.CoreViewModelTest import com.shifthackz.aisdv1.presentation.mocks.mockHuggingFaceModels import com.shifthackz.aisdv1.presentation.mocks.mockLocalAiModels import com.shifthackz.aisdv1.presentation.mocks.mockServerSetupStateLocalModel +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter import com.shifthackz.aisdv1.presentation.stub.stubSchedulersProvider @@ -48,7 +49,7 @@ class ServerSetupViewModelTest : CoreViewModelTest() { private val stubMainRouter = mockk() override fun initializeViewModel() = ServerSetupViewModel( - launchSource = ServerSetupLaunchSource.SETTINGS, + launchSource = LaunchSource.SETTINGS, getConfigurationUseCase = stubGetConfigurationUseCase, getLocalAiModelsUseCase = stubGetLocalAiModelsUseCase, fetchAndGetHuggingFaceModelsUseCase = stubFetchAndGetHuggingFaceModelsUseCase, diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/splash/SplashViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/splash/SplashViewModelTest.kt index a115c47b..6678f581 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/splash/SplashViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/splash/SplashViewModelTest.kt @@ -3,8 +3,8 @@ package com.shifthackz.aisdv1.presentation.screen.splash import com.shifthackz.aisdv1.domain.usecase.splash.SplashNavigationUseCase import com.shifthackz.aisdv1.presentation.core.CoreViewModelInitializeStrategy import com.shifthackz.aisdv1.presentation.core.CoreViewModelTest +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import com.shifthackz.aisdv1.presentation.stub.stubSchedulersProvider import io.mockk.every import io.mockk.mockk @@ -34,7 +34,7 @@ class SplashViewModelTest : CoreViewModelTest() { viewModel.hashCode() verify(inverse = true) { - stubMainRouter.navigateToServerSetup(ServerSetupLaunchSource.SPLASH) + stubMainRouter.navigateToServerSetup(LaunchSource.SPLASH) } verify(inverse = true) { stubMainRouter.navigateToPostSplashConfigLoader() @@ -55,7 +55,7 @@ class SplashViewModelTest : CoreViewModelTest() { verify { stubMainRouter.navigateToServerSetup( - ServerSetupLaunchSource.SPLASH + LaunchSource.SPLASH ) } } diff --git a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageViewModelTest.kt b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageViewModelTest.kt index 25807412..7846c3a5 100644 --- a/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageViewModelTest.kt +++ b/presentation/src/test/java/com/shifthackz/aisdv1/presentation/screen/txt2img/TextToImageViewModelTest.kt @@ -14,9 +14,9 @@ import com.shifthackz.aisdv1.presentation.core.CoreGenerationMviViewModelTest import com.shifthackz.aisdv1.presentation.core.GenerationFormUpdateEvent import com.shifthackz.aisdv1.presentation.core.GenerationMviIntent import com.shifthackz.aisdv1.presentation.mocks.mockAiGenerationResult +import com.shifthackz.aisdv1.presentation.model.LaunchSource import com.shifthackz.aisdv1.presentation.model.Modal import com.shifthackz.aisdv1.presentation.screen.drawer.DrawerIntent -import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupLaunchSource import io.mockk.every import io.mockk.mockk import io.mockk.unmockkAll @@ -455,7 +455,7 @@ class TextToImageViewModelTest : CoreGenerationMviViewModelTest