diff --git a/.github/workflows/android_test.yml b/.github/workflows/android_test.yml index 1333c80f..c19b8eb9 100644 --- a/.github/workflows/android_test.yml +++ b/.github/workflows/android_test.yml @@ -10,8 +10,6 @@ jobs: test-feature: runs-on: ubuntu-latest - needs: build - steps: - name: Checkout code uses: actions/checkout@v4.1.0 diff --git a/README.md b/README.md index c3862e69..8c6bce68 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ User interface of the app is translated for languages listed in this table: | Ukrainian | 0.1.0 | `Translated` | | Turkish | 0.4.1 | `Translated` | | Russian | 0.5.5 | `Translated` | +| Chinese (Simplified) | 0.6.2 | `Translated` | Any contributions to the translations are welcome. diff --git a/app/build.gradle b/app/build.gradle index 998dae94..bc3f0a73 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { namespace 'com.shifthackz.aisdv1.app' defaultConfig { applicationId "com.shifthackz.aisdv1.app" - versionName "0.6.1" - versionCode 180 + versionName "0.6.2" + versionCode 182 buildConfigField "String", "IMAGE_CDN_URL", "\"https://random.imagecdn.app/\"" buildConfigField "String", "HUGGING_FACE_URL", "\"https://huggingface.co/\"" diff --git a/core/validation/src/main/java/com/shifthackz/aisdv1/core/validation/url/UrlValidator.kt b/core/validation/src/main/java/com/shifthackz/aisdv1/core/validation/url/UrlValidator.kt index 8b1f3de1..69bd4305 100644 --- a/core/validation/src/main/java/com/shifthackz/aisdv1/core/validation/url/UrlValidator.kt +++ b/core/validation/src/main/java/com/shifthackz/aisdv1/core/validation/url/UrlValidator.kt @@ -9,6 +9,7 @@ interface UrlValidator { sealed interface Error { data object Empty : Error data object BadScheme : Error + data object BadPort : Error data object Invalid : Error data object Localhost : Error } diff --git a/core/validation/src/main/java/com/shifthackz/aisdv1/core/validation/url/UrlValidatorImpl.kt b/core/validation/src/main/java/com/shifthackz/aisdv1/core/validation/url/UrlValidatorImpl.kt index 4a1ea099..c3e45da2 100644 --- a/core/validation/src/main/java/com/shifthackz/aisdv1/core/validation/url/UrlValidatorImpl.kt +++ b/core/validation/src/main/java/com/shifthackz/aisdv1/core/validation/url/UrlValidatorImpl.kt @@ -3,6 +3,7 @@ package com.shifthackz.aisdv1.core.validation.url import android.util.Patterns import android.webkit.URLUtil import com.shifthackz.aisdv1.core.validation.ValidationResult +import java.net.URI internal class UrlValidatorImpl : UrlValidator { @@ -19,7 +20,11 @@ internal class UrlValidatorImpl : UrlValidator { isValid = false, validationError = UrlValidator.Error.BadScheme, ) - input.contains(LOCALHOST_IPV4) -> ValidationResult( + !isPortValid(input) -> ValidationResult( + isValid = false, + validationError = UrlValidator.Error.BadPort, + ) + isLocalhostUrl(input) -> ValidationResult( isValid = false, validationError = UrlValidator.Error.Localhost, ) @@ -34,9 +39,29 @@ internal class UrlValidatorImpl : UrlValidator { else -> ValidationResult(isValid = true) } + private fun isPortValid(url: String): Boolean = try { + val uri = URI(url) + val port = uri.port + port in 1..65535 || port == -1 + } catch (e: Exception) { + false + } + + private fun isLocalhostUrl(url: String): Boolean = try { + val uri = URI(url) + val host = uri.host + host.equals(LOCALHOST_ALIAS, true) + || host.equals(LOCALHOST_IPV4, true) + || host.equals(LOCALHOST_IPV6, true) + } catch (e: Exception) { + false + } + companion object { private const val SCHEME_HTTPS = "https://" private const val SCHEME_HTTP = "http://" + private const val LOCALHOST_ALIAS = "localhost" private const val LOCALHOST_IPV4 = "127.0.0.1" + private const val LOCALHOST_IPV6 = "[::1]" } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt index 9fff4fc6..0d584946 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/modal/ModalRenderer.kt @@ -192,7 +192,7 @@ fun ModalRenderer( onDismissRequest = dismiss, ) - Modal.ExportInProgress -> ProgressDialog( + Modal.ExportInProgress -> ProgressDialog( titleResId = R.string.exporting_progress_title, subTitleResId = R.string.exporting_progress_sub_title, canDismiss = false, @@ -242,5 +242,14 @@ fun ModalRenderer( onDismissRequest = dismiss, onResult = { processIntent(ImageToImageIntent.UpdateImage(it)) } ) + + Modal.ConnectLocalHost -> DecisionInteractiveDialog( + title = R.string.interaction_warning_title.asUiText(), + text = R.string.interaction_warning_localhost_sub_title.asUiText(), + confirmActionResId = R.string.action_connect, + dismissActionResId = R.string.cancel, + onConfirmAction = { processIntent(ServerSetupIntent.ConnectToLocalHost) }, + onDismissRequest = dismiss, + ) } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt index a73a9c78..a2348ab0 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/model/Modal.kt @@ -22,6 +22,8 @@ sealed interface Modal { data object ExportInProgress : Modal + data object ConnectLocalHost : Modal + @Immutable data class SelectSdModel(val models: List, val selected: String) : Modal @@ -64,13 +66,13 @@ sealed interface Modal { sealed interface Image : Modal { @Immutable - data class Single(val result: AiGenerationResult, val autoSaveEnabled: Boolean): Image + data class Single(val result: AiGenerationResult, val autoSaveEnabled: Boolean) : Image @Immutable - data class Batch(val results: List, val autoSaveEnabled: Boolean): Image + data class Batch(val results: List, val autoSaveEnabled: Boolean) : Image @Immutable - data class Crop(val bitmap: Bitmap): Image + data class Crop(val bitmap: Bitmap) : Image companion object { fun create(list: List, autoSaveEnabled: Boolean): Image = diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupIntent.kt index 2ca7144b..a367ced4 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupIntent.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/ServerSetupIntent.kt @@ -46,6 +46,8 @@ sealed interface ServerSetupIntent : MviIntent { data object LaunchManageStoragePermission : ServerSetupIntent + data object ConnectToLocalHost : ServerSetupIntent + sealed class LaunchUrl : ServerSetupIntent, KoinComponent { protected val linksProvider: LinksProvider by inject() 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 c75a674d..18a72b5e 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 @@ -129,7 +129,7 @@ class ServerSetupViewModel( it.copy(step = ServerSetupState.Step.CONFIGURE) } - ServerSetupState.Step.CONFIGURE -> connectToServer() + ServerSetupState.Step.CONFIGURE -> validateAndConnectToServer() } is ServerSetupIntent.UpdateAuthType -> updateState { @@ -200,10 +200,16 @@ class ServerSetupViewModel( is ServerSetupIntent.UpdateStabilityAiApiKey -> updateState { it.copy(stabilityAiApiKey = intent.key) } + + ServerSetupIntent.ConnectToLocalHost -> connectToServer() } - private fun connectToServer() { + private fun validateAndConnectToServer() { if (!validate()) return + connectToServer() + } + + private fun connectToServer() { emitEffect(ServerSetupEffect.HideKeyboard) !when (currentState.mode) { ServerSource.HORDE -> connectToHorde() @@ -212,8 +218,10 @@ class ServerSetupViewModel( ServerSource.HUGGING_FACE -> connectToHuggingFace() ServerSource.OPEN_AI -> connectToOpenAi() ServerSource.STABILITY_AI -> connectToStabilityAi() - }.doOnSubscribe { setScreenModal(Modal.Communicating(canCancel = false)) } - .subscribeOnMainThread(schedulersProvider).subscribeBy(::errorLog) { result -> + } + .doOnSubscribe { setScreenModal(Modal.Communicating(canCancel = false)) } + .subscribeOnMainThread(schedulersProvider) + .subscribeBy(::errorLog) { result -> result.fold( onSuccess = { onSetupComplete() }, onFailure = { t -> @@ -230,8 +238,8 @@ class ServerSetupViewModel( else { val serverUrlValidation = urlValidator(currentState.serverUrl) var isValid = serverUrlValidation.isValid - updateState { - var newState = it.copy( + updateState { state -> + var newState = state.copy( serverUrlValidationError = serverUrlValidation.mapToUi() ) if (currentState.authType == ServerSetupState.AuthType.HTTP_BASIC) { @@ -243,6 +251,12 @@ class ServerSetupViewModel( ) isValid = isValid && loginValidation.isValid && passwordValidation.isValid } + if (serverUrlValidation.validationError is UrlValidator.Error.Localhost + && newState.loginValidationError == null + && newState.passwordValidationError == null + ) { + newState = newState.copy(screenModal = Modal.ConnectLocalHost) + } newState } isValid diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/forms/Automatic1111Form.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/forms/Automatic1111Form.kt index 670bfccc..2614a70e 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/forms/Automatic1111Form.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/forms/Automatic1111Form.kt @@ -66,11 +66,12 @@ fun Automatic1111Form( supportingText = state.serverUrlValidationError ?.takeIf { !state.demoMode } ?.let { { Text(it.asString(), color = MaterialTheme.colorScheme.error) } }, + maxLines = 1, ) if (!state.demoMode) { DropdownTextField( modifier = fieldModifier, - label = "Authorization".asUiText(), + label = R.string.auth_title.asUiText(), items = ServerSetupState.AuthType.entries, value = state.authType, onItemSelected = { @@ -85,7 +86,6 @@ fun Automatic1111Form( ) when (state.authType) { ServerSetupState.AuthType.HTTP_BASIC -> { - TextField( modifier = fieldModifier, value = state.login, @@ -97,6 +97,7 @@ fun Automatic1111Form( supportingText = state.loginValidationError?.let { { Text(it.asString(), color = MaterialTheme.colorScheme.error) } }, + maxLines = 1, ) TextField( modifier = fieldModifier, @@ -107,8 +108,11 @@ fun Automatic1111Form( label = { Text(stringResource(id = R.string.hint_password)) }, isError = state.passwordValidationError != null, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - visualTransformation = if (state.passwordVisible) VisualTransformation.None - else PasswordVisualTransformation(), + visualTransformation = if (state.passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, supportingText = state.passwordValidationError?.let { { Text(it.asString(), color = MaterialTheme.colorScheme.error) } }, @@ -126,7 +130,8 @@ fun Automatic1111Form( }, content = { Icon(image, description) }, ) - } + }, + maxLines = 1, ) } else -> Unit @@ -171,4 +176,4 @@ fun Automatic1111Form( style = MaterialTheme.typography.bodyMedium, ) } -} \ No newline at end of file +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/mappers/ServerSetupValidationUrlMapper.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/mappers/ServerSetupValidationUrlMapper.kt index a2d65580..0529ee08 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/mappers/ServerSetupValidationUrlMapper.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/setup/mappers/ServerSetupValidationUrlMapper.kt @@ -10,8 +10,9 @@ fun ValidationResult.mapToUi(): UiText? { if (this.isValid) return null return when (validationError as UrlValidator.Error) { UrlValidator.Error.BadScheme -> R.string.error_invalid_scheme + UrlValidator.Error.BadPort -> R.string.error_invalid_port UrlValidator.Error.Empty -> R.string.error_empty_url UrlValidator.Error.Invalid -> R.string.error_invalid_url - UrlValidator.Error.Localhost -> R.string.error_localhost_url - }.asUiText() + UrlValidator.Error.Localhost -> null + }?.asUiText() } diff --git a/presentation/src/main/res/values-ru/strings.xml b/presentation/src/main/res/values-ru/strings.xml index a04b15c7..17830fbf 100644 --- a/presentation/src/main/res/values-ru/strings.xml +++ b/presentation/src/main/res/values-ru/strings.xml @@ -89,6 +89,7 @@ Область Inpaint Авто + Авторизация Анонимно HTTP Базовая @@ -208,6 +209,9 @@ Эта функция экспортирует все изображения галереи в архив *.zip. Этот процесс может длиться долго, если у вас много изображений. Хотите продолжить? Это приведет к сбросу настроек программы и удалению всех созданных изображений. Вы хотите продолжить? + Предупреждение + Вы пытаетесь подключиться к локальному серверу localhost (127.0.0.1).\n\nЭто может не сработать, если на вашем Android-устройстве не настроено туннелирование ssh или другой механизм переадресации портов. + Удалить изображение Вы уверены, что хотите окончательно удалить это изображение? @@ -230,6 +234,7 @@ Поле не может быть пустым Неверные данные URL должно начинаться с http:// или https:// + Порт должен быть в диапазоне от 0 до 65535 Недействительный URL-адрес сервера Вы не можете использовать localhost (127.0.0.1) URL-адрес для подключения к серверу. Минимальный размер %1$s diff --git a/presentation/src/main/res/values-tr/strings.xml b/presentation/src/main/res/values-tr/strings.xml index 532b023c..9ebc6821 100644 --- a/presentation/src/main/res/values-tr/strings.xml +++ b/presentation/src/main/res/values-tr/strings.xml @@ -89,13 +89,14 @@ Inpaint alan Otomatik olarak algıla + Yetki Anonim HTTP Temel Lütfen Stable Diffusion WebUI(AUTOMATIC1111) URL adresinizi yazın. Bazı sunucu örnekleri:\n• http://192.168.0.2:7860\n• http://alanadiniz.com:7860\n• https://alanadiniz.com This mode allows you to test the application behavior, even if you don\'t have Stable Diffusion WebUI server.\n\nIn demo mode app ignores user prompt, does not use AI server, and returns some mock images. - Before connecting ensure that:\n• you are running AUTOMATIC1111 WebUI with flags --api --listen\n• your firewall is not blocking 7860 port\n• phone is on the same WiFi with your PC + Before connecting ensure that:\n• you are running AUTOMATIC1111 WebUI with flags --api --listen\n• your firewall is not blocking 7860 port\n• phone is on the same WiFi with your PC Horde AI bulutuna bağlanın Horde AI, Görüntü oluşturma çalışanları ve metin oluşturma çalışanlarından oluşan kitle kaynaklı dağıtılmış bir kümedir. @@ -128,7 +129,7 @@ Ayarlar Resim - Orjinal + Orijinal Bilgi Yazıdan Resime @@ -208,6 +209,9 @@ Bu işlem bütün galerideki resimleri tek bir .zip arşivi dosyası olarak dışa aktaracaktır. Galerinizin boyutuna göre bu işlem uzun bir zaman alabailir. Devam etmek istiyor musunuz? Bu işlem bütün uygulama ayarlarını ve oluşturulan resimleri silecektir. Devam etmek istiyor musunuz? + Uyarı + Localhost (127.0.0.1) sunucusuna bağlanmaya çalışıyorsunuz.\n\nAndroid cihazınızda ssh tüneli veya başka bir bağlantı noktası yönlendirme mekanizması kurulu olmadığı sürece çalışmayabilir. + Resmi sil Kalıcı olarak bu resmi silmek istediğinize emin misiniz? @@ -230,6 +234,7 @@ Alan boş olmamalıdır Geçerli olmayan bilgi girişi Sunucu adresi http:// veya https:// ile başlamalıdır. + Port 0 ile 65535 aralığında olmalıdır Geçersiz sunucu adresi. Sunucuya bağlanmak için localhost (127.0.0.1) URL adresi kullanamazsınız. Asgari boyut %1$s diff --git a/presentation/src/main/res/values-uk/strings.xml b/presentation/src/main/res/values-uk/strings.xml index b974d210..2b458faa 100644 --- a/presentation/src/main/res/values-uk/strings.xml +++ b/presentation/src/main/res/values-uk/strings.xml @@ -89,6 +89,7 @@ Область Inpaint Авто + Авторизація Анонімна HTTP Базова @@ -208,6 +209,9 @@ Ця функція експортує всі зображення галереї у архів *.zip. Цей процес може тривати довго, якщо у вас багато зображень. Бажаєте продовжити? Це призведе до скидання налаштувань програми та видалення всіх створених зображень. Ви бажаєте продовжити? + Попередження + Ви намагаєтеся підключитися до локального сервера localhost (127.0.0.1).\n\nЦе може не працювати, якщо на вашому пристрої Android не налаштовано SSH-тунелювання або інший механізм переадресації портів. + Видалити зображення Ви впевнені, що хочете остаточно видалити це зображення? @@ -230,6 +234,7 @@ Поле не може бути пустим Неправильні дані URL має починатися з http:// або https:// + Порт має бути в діапазоні від 0 до 65535 Недійсна URL-адреса сервера Ви не можете використовувати localhost (127.0.0.1) URL-адресу для підключення до сервера. Мінімальний розмір %1$s diff --git a/presentation/src/main/res/values-zh/strings.xml b/presentation/src/main/res/values-zh/strings.xml index 230d7bf8..e2d9c8bd 100644 --- a/presentation/src/main/res/values-zh/strings.xml +++ b/presentation/src/main/res/values-zh/strings.xml @@ -115,6 +115,7 @@ 自动检测 + 授权 匿名 HTTP基本认证 @@ -255,6 +256,10 @@ 这将把所有图库图像导出为*.zip压缩包。此过程可能很长,如果您有很多图片,您想继续吗? 这将重置应用设置并删除所有生成的图像。您想继续吗? + + 警告性的 + 您正在尝试连接到本地主机(127.0.0.1)服务器。\n\n除非您的 Android 设备上设置了 ssh 隧道或其他端口转发机制,否则它可能无法工作。 + 删除图像 您确定要永久删除此图像吗? @@ -283,6 +288,7 @@ 字段不应为空 无效输入 服务器URL应以http://https://开头 + 端口范围应为 0 至 65535 无效的服务器URL 您不能使用本地主机(127.0.0.1)URL来连接到服务器。 最小尺寸为 %1$s diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index ce6f3cf4..c3b31a7b 100755 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -105,13 +105,14 @@ Inpaint area Detect automatically + Authorization Anonymous HTTP Basic Provide your Stable Diffusion WebUI URL Here are the examples of server URLs:\n• http://192.168.0.2:7860\n• http://yourdomain.com:7860\n• https://yourdomain.com This mode allows you to test the application behavior, even if you don\'t have Stable Diffusion WebUI server.\n\nIn demo mode app ignores user prompt, does not use AI server, and returns some mock images. - Before connecting ensure that:\n• you are running AUTOMATIC1111 WebUI with flags --api --listen\n• your firewall is not blocking 7860 port\n• phone is on the same WiFi with your PC + Before connecting ensure that:\n• you are running AUTOMATIC1111 WebUI with flags --api --listen\n• your firewall is not blocking 7860 port\n• phone is on the same WiFi with your PC Connect to Horde AI cloud Horde AI is a crowdsourced distributed cluster of Image generation workers and text generation workers. @@ -227,6 +228,9 @@ This will export all the gallery images as *.zip archive. This process may be long if you have many images, would you like to proceed? This will reset app settings and delete all the generated images. Do you want to proceed? + Warning + You are trying to connect to localhost (127.0.0.1) server.\n\nIt may not work unless you have ssh tunneling or another port forwarding mechanism set up on your Android device. + Delete image Are you sure you want to permanently delete this image? @@ -249,6 +253,7 @@ Field should not be empty Invalid input Server URL should start with http:// or https:// + Port should be in range from 0 to 65535 Invalid Server URL You can not use localhost (127.0.0.1) URL to connect to server. Minimum size is %1$s