diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2561ec75..13c225ed 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,6 +26,7 @@ android { buildConfigField("String", "OPEN_AI_INFO_URL", "\"https://platform.openai.com/api-keys\"") buildConfigField("String", "STABILITY_AI_INFO_URL", "\"https://platform.stability.ai/\"") buildConfigField("String", "UPDATE_API_URL", "\"https://sdai.moroz.cc\"") + buildConfigField("String", "REPORT_API_URL", "\"https://sdai-report.moroz.cc\"") buildConfigField("String", "DEMO_MODE_API_URL", "\"https://sdai.moroz.cc\"") buildConfigField("String", "POLICY_URL", "\"https://sdai.moroz.cc/policy.html\"") buildConfigField("String", "DONATE_URL", "\"https://www.buymeacoffee.com/shifthackz\"") diff --git a/app/src/main/java/com/shifthackz/aisdv1/app/di/ProvidersModule.kt b/app/src/main/java/com/shifthackz/aisdv1/app/di/ProvidersModule.kt index c9b147fb..5aaa0ea2 100755 --- a/app/src/main/java/com/shifthackz/aisdv1/app/di/ProvidersModule.kt +++ b/app/src/main/java/com/shifthackz/aisdv1/app/di/ProvidersModule.kt @@ -48,6 +48,7 @@ val providersModule = module { object : ApiUrlProvider { override val stableDiffusionAutomaticApiUrl: String = DEFAULT_SERVER_URL override val stableDiffusionAppApiUrl: String = BuildConfig.UPDATE_API_URL + override val stableDiffusionReportApiUrl: String = BuildConfig.REPORT_API_URL override val hordeApiUrl: String = BuildConfig.HORDE_AI_URL override val imageCdnApiUrl: String = BuildConfig.IMAGE_CDN_URL override val huggingFaceApiUrl: String = BuildConfig.HUGGING_FACE_URL diff --git a/core/localization/src/main/res/values-ru/strings.xml b/core/localization/src/main/res/values-ru/strings.xml index ef0e1272..97ad41fd 100644 --- a/core/localization/src/main/res/values-ru/strings.xml +++ b/core/localization/src/main/res/values-ru/strings.xml @@ -361,4 +361,17 @@ [Офлайн] генерация\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 8319a6c1..556aaaec 100644 --- a/core/localization/src/main/res/values-tr/strings.xml +++ b/core/localization/src/main/res/values-tr/strings.xml @@ -361,4 +361,17 @@ [Çevrimdışı] nesil\nYerel Dağıtım. Uygulamayı özelleştirin,\n[kendinizin] yapın! Nesil sağlayıcıyı\nseçme [Özgürlüğü]. + + Görüntüyü Şikayet Et + Şikayeti Gönder + Şikayet açıklaması + Şikayetiniz gönderildi! + Geri dön + + Uygunsuz içerik + Şiddet + Nefret söylemi + Fikri mülkiyet ihlali + Yetişkin içeriği + Diğer diff --git a/core/localization/src/main/res/values-uk/strings.xml b/core/localization/src/main/res/values-uk/strings.xml index 87fe4b5d..175a734e 100644 --- a/core/localization/src/main/res/values-uk/strings.xml +++ b/core/localization/src/main/res/values-uk/strings.xml @@ -361,4 +361,17 @@ [Офлайн] генерація\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 7329dc23..8b493b5d 100644 --- a/core/localization/src/main/res/values-zh/strings.xml +++ b/core/localization/src/main/res/values-zh/strings.xml @@ -427,4 +427,17 @@ [离线]生成\n本地扩散。 自定义应用程序,\n让它成为[您的]! [自由]选择\n代提供商。 + + 举报图片 + 提交举报 + 举报描述 + 您的举报已发送! + 返回 + + 不适当内容 + 暴力 + 仇恨言论 + 知识产权侵犯 + 成人内容 + 其他 diff --git a/core/localization/src/main/res/values/strings.xml b/core/localization/src/main/res/values/strings.xml index b591977e..8263cc8f 100755 --- a/core/localization/src/main/res/values/strings.xml +++ b/core/localization/src/main/res/values/strings.xml @@ -388,4 +388,17 @@ [Offline] Local Diffusion\nAI generation. Configure, customize,\nmake it [yours]! [Freedom] to choose your\nAI generation provider. + + Report Image + Submit Report + Report description + Your report was sent! + Go back + + Inappropriate content + Violence + Hateful speech + Intellectual property infringement + Adult content + Other diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/di/RemoteDataSourceModule.kt b/data/src/main/java/com/shifthackz/aisdv1/data/di/RemoteDataSourceModule.kt index ea6377c4..29912afd 100755 --- a/data/src/main/java/com/shifthackz/aisdv1/data/di/RemoteDataSourceModule.kt +++ b/data/src/main/java/com/shifthackz/aisdv1/data/di/RemoteDataSourceModule.kt @@ -10,6 +10,7 @@ import com.shifthackz.aisdv1.data.remote.HuggingFaceGenerationRemoteDataSource import com.shifthackz.aisdv1.data.remote.HuggingFaceModelsRemoteDataSource import com.shifthackz.aisdv1.data.remote.OpenAiGenerationRemoteDataSource import com.shifthackz.aisdv1.data.remote.RandomImageRemoteDataSource +import com.shifthackz.aisdv1.data.remote.ReportRemoteDataSource import com.shifthackz.aisdv1.data.remote.ServerConfigurationRemoteDataSource import com.shifthackz.aisdv1.data.remote.StabilityAiCreditsRemoteDataSource import com.shifthackz.aisdv1.data.remote.StabilityAiEnginesRemoteDataSource @@ -34,6 +35,7 @@ import com.shifthackz.aisdv1.domain.datasource.HuggingFaceModelsDataSource import com.shifthackz.aisdv1.domain.datasource.LorasDataSource import com.shifthackz.aisdv1.domain.datasource.OpenAiGenerationDataSource import com.shifthackz.aisdv1.domain.datasource.RandomImageDataSource +import com.shifthackz.aisdv1.domain.datasource.ReportDataSource import com.shifthackz.aisdv1.domain.datasource.ServerConfigurationDataSource import com.shifthackz.aisdv1.domain.datasource.StabilityAiCreditsDataSource import com.shifthackz.aisdv1.domain.datasource.StabilityAiEnginesDataSource @@ -94,6 +96,7 @@ val remoteDataSourceModule = module { factoryOf(::StabilityAiGenerationRemoteDataSource) bind StabilityAiGenerationDataSource.Remote::class factoryOf(::StabilityAiCreditsRemoteDataSource) bind StabilityAiCreditsDataSource.Remote::class factoryOf(::StabilityAiEnginesRemoteDataSource) bind StabilityAiEnginesDataSource.Remote::class + factoryOf(::ReportRemoteDataSource) bind ReportDataSource.Remote::class factory { val lambda: () -> Boolean = { diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/di/RepositoryModule.kt b/data/src/main/java/com/shifthackz/aisdv1/data/di/RepositoryModule.kt index 21d8edda..1358c8a9 100755 --- a/data/src/main/java/com/shifthackz/aisdv1/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/shifthackz/aisdv1/data/di/RepositoryModule.kt @@ -13,6 +13,7 @@ import com.shifthackz.aisdv1.data.repository.LorasRepositoryImpl import com.shifthackz.aisdv1.data.repository.MediaPipeGenerationRepositoryImpl import com.shifthackz.aisdv1.data.repository.OpenAiGenerationRepositoryImpl import com.shifthackz.aisdv1.data.repository.RandomImageRepositoryImpl +import com.shifthackz.aisdv1.data.repository.ReportRepositoryImpl import com.shifthackz.aisdv1.data.repository.ServerConfigurationRepositoryImpl import com.shifthackz.aisdv1.data.repository.StabilityAiCreditsRepositoryImpl import com.shifthackz.aisdv1.data.repository.StabilityAiEnginesRepositoryImpl @@ -37,6 +38,7 @@ import com.shifthackz.aisdv1.domain.repository.LorasRepository import com.shifthackz.aisdv1.domain.repository.MediaPipeGenerationRepository import com.shifthackz.aisdv1.domain.repository.OpenAiGenerationRepository import com.shifthackz.aisdv1.domain.repository.RandomImageRepository +import com.shifthackz.aisdv1.domain.repository.ReportRepository import com.shifthackz.aisdv1.domain.repository.ServerConfigurationRepository import com.shifthackz.aisdv1.domain.repository.StabilityAiCreditsRepository import com.shifthackz.aisdv1.domain.repository.StabilityAiEnginesRepository @@ -86,4 +88,5 @@ val repositoryModule = module { factoryOf(::DownloadableModelRepositoryImpl) bind DownloadableModelRepository::class factoryOf(::HuggingFaceModelsRepositoryImpl) bind HuggingFaceModelsRepository::class factoryOf(::SupportersRepositoryImpl) bind SupportersRepository::class + factoryOf(::ReportRepositoryImpl) bind ReportRepository::class } diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/remote/ReportRemoteDataSource.kt b/data/src/main/java/com/shifthackz/aisdv1/data/remote/ReportRemoteDataSource.kt new file mode 100644 index 00000000..a7ec462d --- /dev/null +++ b/data/src/main/java/com/shifthackz/aisdv1/data/remote/ReportRemoteDataSource.kt @@ -0,0 +1,21 @@ +package com.shifthackz.aisdv1.data.remote + +import com.shifthackz.aisdv1.domain.datasource.ReportDataSource +import com.shifthackz.aisdv1.domain.entity.ReportReason +import com.shifthackz.aisdv1.network.api.sdai.ReportApi +import com.shifthackz.aisdv1.network.request.ReportRequest +import io.reactivex.rxjava3.core.Completable + +internal class ReportRemoteDataSource(private val api: ReportApi) : ReportDataSource.Remote { + + override fun send( + text: String, + reason: ReportReason, + image: String, + source: String, + model: String + ): Completable { + val payload = ReportRequest(text, reason.toString(), image, source, model) + return api.postReport(payload) + } +} diff --git a/data/src/main/java/com/shifthackz/aisdv1/data/repository/ReportRepositoryImpl.kt b/data/src/main/java/com/shifthackz/aisdv1/data/repository/ReportRepositoryImpl.kt new file mode 100644 index 00000000..0abfd08b --- /dev/null +++ b/data/src/main/java/com/shifthackz/aisdv1/data/repository/ReportRepositoryImpl.kt @@ -0,0 +1,26 @@ +package com.shifthackz.aisdv1.data.repository + +import com.shifthackz.aisdv1.domain.datasource.ReportDataSource +import com.shifthackz.aisdv1.domain.entity.ReportReason +import com.shifthackz.aisdv1.domain.entity.ServerSource +import com.shifthackz.aisdv1.domain.preference.PreferenceManager +import com.shifthackz.aisdv1.domain.repository.ReportRepository +import io.reactivex.rxjava3.core.Completable + +internal class ReportRepositoryImpl( + private val rds: ReportDataSource.Remote, + private val preferenceManager: PreferenceManager, +) : ReportRepository { + + override fun send(text: String, reason: ReportReason, image: String): Completable { + val source = preferenceManager.source + val model = when (source) { + ServerSource.HUGGING_FACE -> preferenceManager.huggingFaceModel + ServerSource.STABILITY_AI -> preferenceManager.stabilityAiEngineId + ServerSource.LOCAL_MICROSOFT_ONNX -> preferenceManager.localOnnxModelId + ServerSource.LOCAL_GOOGLE_MEDIA_PIPE -> preferenceManager.localMediaPipeModelId + else -> "" + } + return rds.send(text, reason, image, source.toString(), model) + } +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/datasource/ReportDataSource.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/datasource/ReportDataSource.kt new file mode 100644 index 00000000..7547588d --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/datasource/ReportDataSource.kt @@ -0,0 +1,18 @@ +package com.shifthackz.aisdv1.domain.datasource + +import com.shifthackz.aisdv1.domain.entity.ReportReason +import io.reactivex.rxjava3.core.Completable + +sealed interface ReportDataSource { + + interface Remote : ReportDataSource { + + fun send( + text: String, + reason: ReportReason, + image: String, + source: String, + model: String, + ): Completable + } +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/di/DomainModule.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/di/DomainModule.kt index 5bdaf74e..f3bdb8e5 100755 --- a/domain/src/main/java/com/shifthackz/aisdv1/domain/di/DomainModule.kt +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/di/DomainModule.kt @@ -74,6 +74,8 @@ import com.shifthackz.aisdv1.domain.usecase.generation.TextToImageUseCase import com.shifthackz.aisdv1.domain.usecase.generation.TextToImageUseCaseImpl import com.shifthackz.aisdv1.domain.usecase.huggingface.FetchAndGetHuggingFaceModelsUseCase import com.shifthackz.aisdv1.domain.usecase.huggingface.FetchAndGetHuggingFaceModelsUseCaseImpl +import com.shifthackz.aisdv1.domain.usecase.report.SendReportUseCase +import com.shifthackz.aisdv1.domain.usecase.report.SendReportUseCaseImpl import com.shifthackz.aisdv1.domain.usecase.sdembedding.FetchAndGetEmbeddingsUseCase import com.shifthackz.aisdv1.domain.usecase.sdembedding.FetchAndGetEmbeddingsUseCaseImpl import com.shifthackz.aisdv1.domain.usecase.sdhypernet.FetchAndGetHyperNetworksUseCase @@ -179,6 +181,7 @@ internal val useCasesModule = module { factoryOf(::ObserveStabilityAiCreditsUseCaseImpl) bind ObserveStabilityAiCreditsUseCase::class factoryOf(::FetchAndGetStabilityAiEnginesUseCaseImpl) bind FetchAndGetStabilityAiEnginesUseCase::class factoryOf(::FetchAndGetSupportersUseCaseImpl) bind FetchAndGetSupportersUseCase::class + factoryOf(::SendReportUseCaseImpl) bind SendReportUseCase::class } internal val interActorsModule = module { diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/ReportReason.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/ReportReason.kt new file mode 100644 index 00000000..bda8a456 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/entity/ReportReason.kt @@ -0,0 +1,10 @@ +package com.shifthackz.aisdv1.domain.entity + +enum class ReportReason { + IntellectualPropertyInfringement, + Violence, + InappropriateContent, + AdultContent, + HatefulSpeech, + Other; +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/ReportRepository.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/ReportRepository.kt new file mode 100644 index 00000000..dc4ae339 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/repository/ReportRepository.kt @@ -0,0 +1,8 @@ +package com.shifthackz.aisdv1.domain.repository + +import com.shifthackz.aisdv1.domain.entity.ReportReason +import io.reactivex.rxjava3.core.Completable + +interface ReportRepository { + fun send(text: String, reason: ReportReason, image: String): Completable +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/report/SendReportUseCase.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/report/SendReportUseCase.kt new file mode 100644 index 00000000..1e4cae10 --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/report/SendReportUseCase.kt @@ -0,0 +1,8 @@ +package com.shifthackz.aisdv1.domain.usecase.report + +import com.shifthackz.aisdv1.domain.entity.ReportReason +import io.reactivex.rxjava3.core.Completable + +interface SendReportUseCase { + operator fun invoke(text: String, reason: ReportReason, image: String): Completable +} diff --git a/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/report/SendReportUseCaseImpl.kt b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/report/SendReportUseCaseImpl.kt new file mode 100644 index 00000000..301e905b --- /dev/null +++ b/domain/src/main/java/com/shifthackz/aisdv1/domain/usecase/report/SendReportUseCaseImpl.kt @@ -0,0 +1,13 @@ +package com.shifthackz.aisdv1.domain.usecase.report + +import com.shifthackz.aisdv1.domain.entity.ReportReason +import com.shifthackz.aisdv1.domain.repository.ReportRepository + +class SendReportUseCaseImpl( + private val repository: ReportRepository, +) : SendReportUseCase { + + override fun invoke(text: String, reason: ReportReason, image: String) = + repository.send(text, reason, image) + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5d13254a..965bd7fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -versionName = "0.6.4" -versionCode = "184" +versionName = "0.6.5" +versionCode = "185" targetSdk = "34" -compileSdk = "34" +compileSdk = "35" minSdk = "24" agp = "8.7.2" kotlin = "2.0.21" diff --git a/network/src/main/java/com/shifthackz/aisdv1/network/api/sdai/ReportApi.kt b/network/src/main/java/com/shifthackz/aisdv1/network/api/sdai/ReportApi.kt new file mode 100644 index 00000000..5c4c6015 --- /dev/null +++ b/network/src/main/java/com/shifthackz/aisdv1/network/api/sdai/ReportApi.kt @@ -0,0 +1,12 @@ +package com.shifthackz.aisdv1.network.api.sdai + +import com.shifthackz.aisdv1.network.request.ReportRequest +import io.reactivex.rxjava3.core.Completable +import retrofit2.http.Body +import retrofit2.http.POST + +interface ReportApi { + + @POST("/report") + fun postReport(@Body request: ReportRequest): Completable +} diff --git a/network/src/main/java/com/shifthackz/aisdv1/network/di/NetworkModule.kt b/network/src/main/java/com/shifthackz/aisdv1/network/di/NetworkModule.kt index 92ffeb5b..eb6a3528 100755 --- a/network/src/main/java/com/shifthackz/aisdv1/network/di/NetworkModule.kt +++ b/network/src/main/java/com/shifthackz/aisdv1/network/di/NetworkModule.kt @@ -15,6 +15,7 @@ import com.shifthackz.aisdv1.network.api.sdai.DonateApi import com.shifthackz.aisdv1.network.api.sdai.DownloadableModelsApi import com.shifthackz.aisdv1.network.api.sdai.DownloadableModelsApiImpl import com.shifthackz.aisdv1.network.api.sdai.HuggingFaceModelsApi +import com.shifthackz.aisdv1.network.api.sdai.ReportApi import com.shifthackz.aisdv1.network.api.stabilityai.StabilityAiApi import com.shifthackz.aisdv1.network.api.swarmui.SwarmUiApi import com.shifthackz.aisdv1.network.api.swarmui.SwarmUiApiImpl @@ -143,6 +144,12 @@ val networkModule = module { .create(DonateApi::class.java) } + single { + get() + .withBaseUrl(get().stableDiffusionReportApiUrl) + .create(ReportApi::class.java) + } + single { get() .withBaseUrl(get().imageCdnApiUrl) diff --git a/network/src/main/java/com/shifthackz/aisdv1/network/qualifiers/ApiUrlProvider.kt b/network/src/main/java/com/shifthackz/aisdv1/network/qualifiers/ApiUrlProvider.kt index d6de2d22..90fd00be 100755 --- a/network/src/main/java/com/shifthackz/aisdv1/network/qualifiers/ApiUrlProvider.kt +++ b/network/src/main/java/com/shifthackz/aisdv1/network/qualifiers/ApiUrlProvider.kt @@ -3,6 +3,7 @@ package com.shifthackz.aisdv1.network.qualifiers interface ApiUrlProvider { val stableDiffusionAutomaticApiUrl: String val stableDiffusionAppApiUrl: String + val stableDiffusionReportApiUrl: String val hordeApiUrl: String val imageCdnApiUrl: String val huggingFaceApiUrl: String diff --git a/network/src/main/java/com/shifthackz/aisdv1/network/request/ReportRequest.kt b/network/src/main/java/com/shifthackz/aisdv1/network/request/ReportRequest.kt new file mode 100644 index 00000000..0d5fecba --- /dev/null +++ b/network/src/main/java/com/shifthackz/aisdv1/network/request/ReportRequest.kt @@ -0,0 +1,16 @@ +package com.shifthackz.aisdv1.network.request + +import com.google.gson.annotations.SerializedName + +data class ReportRequest( + @SerializedName("text") + val text: String, + @SerializedName("reason") + val reason: String, + @SerializedName("image") + val image: String, + @SerializedName("server_source") + val serverSource: String, + @SerializedName("model") + val model: String, +) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviIntent.kt index 39298c8e..e496b190 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviIntent.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/core/GenerationMviIntent.kt @@ -76,6 +76,8 @@ sealed interface GenerationMviIntent : MviIntent { data class Save(val ai: List) : Result data class View(val ai: AiGenerationResult) : Result + + data class Report(val ai: AiGenerationResult) : Result } data class SetModal(val modal: Modal) : GenerationMviIntent 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 689fc79c..bed64e74 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 @@ -231,6 +231,8 @@ abstract class GenerationMviViewModel mainRouter.navigateToReportImage(intent.ai.id) + is GenerationMviIntent.SetModal -> setActiveModal(intent.modal) GenerationMviIntent.Cancel.Generation -> { 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 c2c8ee8b..d0d88d68 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 @@ -17,6 +17,7 @@ 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.report.ReportViewModel import com.shifthackz.aisdv1.presentation.screen.settings.SettingsViewModel import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupViewModel import com.shifthackz.aisdv1.presentation.screen.splash.SplashViewModel @@ -103,4 +104,16 @@ val viewModelModule = module { mainRouter = get(), ) } + + viewModel { parameters -> + ReportViewModel( + itemId = parameters.get(), + sendReportUseCase = get(), + getGenerationResultUseCase = get(), + getLastResultFromCacheUseCase = get(), + base64ToBitmapConverter = get(), + mainRouter = get(), + schedulersProvider = get(), + ) + } } 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 5acbcd6d..940e930b 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 @@ -35,6 +35,7 @@ import com.shifthackz.aisdv1.presentation.screen.debug.DebugMenuIntent import com.shifthackz.aisdv1.presentation.screen.gallery.detail.GalleryDetailIntent import com.shifthackz.aisdv1.presentation.screen.gallery.list.GalleryIntent import com.shifthackz.aisdv1.presentation.screen.inpaint.InPaintIntent +import com.shifthackz.aisdv1.presentation.screen.report.ReportIntent import com.shifthackz.aisdv1.presentation.screen.settings.SettingsIntent import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupIntent import com.shifthackz.aisdv1.presentation.widget.dialog.DecisionInteractiveDialog @@ -61,6 +62,7 @@ fun ModalRenderer( processIntent(GalleryDetailIntent.DismissDialog) processIntent(InPaintIntent.ScreenModal.Dismiss) processIntent(DebugMenuIntent.DismissModal) + processIntent(ReportIntent.DismissError) } val context = LocalContext.current when (screenModal) { @@ -113,6 +115,9 @@ fun ModalRenderer( onSaveRequest = { processIntent(GenerationMviIntent.Result.Save(listOf(screenModal.result))) }, + onReportRequest = { + processIntent(GenerationMviIntent.Result.Report(screenModal.result)) + }, onViewDetailRequest = { processIntent(GenerationMviIntent.Result.View(screenModal.result)) }, diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/NavigationRoute.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/NavigationRoute.kt index 2ff81d92..d898633b 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/NavigationRoute.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/NavigationRoute.kt @@ -34,6 +34,9 @@ sealed interface NavigationRoute { @Serializable data class GalleryDetail(val itemId: Long) : NavigationRoute + @Serializable + data class ReportImage(val itemId: Long) : NavigationRoute + @Serializable data class ServerSetup(val source: LaunchSource) : NavigationRoute 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 0da1271f..e9de82a6 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 @@ -14,6 +14,8 @@ import com.shifthackz.aisdv1.presentation.screen.loader.ConfigurationLoaderScree import com.shifthackz.aisdv1.presentation.screen.logger.LoggerScreen import com.shifthackz.aisdv1.presentation.screen.onboarding.OnBoardingScreen import com.shifthackz.aisdv1.presentation.screen.onboarding.OnBoardingViewModel +import com.shifthackz.aisdv1.presentation.screen.report.ReportScreen +import com.shifthackz.aisdv1.presentation.screen.report.ReportViewModel import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupScreen import com.shifthackz.aisdv1.presentation.screen.setup.ServerSetupViewModel import com.shifthackz.aisdv1.presentation.screen.splash.SplashScreen @@ -24,9 +26,11 @@ import org.koin.core.parameter.parametersOf import kotlin.reflect.typeOf fun NavGraphBuilder.mainNavGraph() { + composable { SplashScreen() } + composable( typeMap = mapOf( typeOf() to NavType.EnumType(LaunchSource::class.java) @@ -40,29 +44,47 @@ fun NavGraphBuilder.mainNavGraph() { buildInfoProvider = koinInject() ) } + composable { ConfigurationLoaderScreen() } + homeScreenNavGraph() + composable { entry -> val itemId = entry.toRoute().itemId GalleryDetailScreen(itemId = itemId) } + + composable { entry -> + val itemId = entry.toRoute().itemId + ReportScreen( + viewModel = koinViewModel( + parameters = { parametersOf(itemId) } + ), + ) + } + composable { DebugMenuScreen() } + composable { LoggerScreen() } + composable { InPaintScreen() } + composable { WebUiScreen() } + composable { DonateScreen() } + composable( typeMap = mapOf( typeOf() to NavType.EnumType(LaunchSource::class.java) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/navigation/router/main/MainRouter.kt index b45a025c..ed63759d 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 @@ -18,6 +18,8 @@ interface MainRouter : Router { fun navigateToGalleryDetails(itemId: Long) + fun navigateToReportImage(itemId: Long) + fun navigateToInPaint() fun navigateToDonate() 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 847bd098..2a10039a 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 @@ -67,6 +67,14 @@ internal class MainRouterImpl : MainRouter { ) } + override fun navigateToReportImage(itemId: Long) { + effectSubject.onNext( + NavigationEffect.Navigate.Route( + navRoute = NavigationRoute.ReportImage(itemId = itemId) + ) + ) + } + override fun navigateToInPaint() { effectSubject.onNext(NavigationEffect.Navigate.Route(navRoute = NavigationRoute.InPaint)) } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailIntent.kt index ba3bc591..419a444a 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailIntent.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailIntent.kt @@ -22,5 +22,7 @@ sealed interface GalleryDetailIntent : MviIntent { Request, Confirm } + data object Report : GalleryDetailIntent + data object DismissDialog : GalleryDetailIntent } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailScreen.kt index 3212a9a6..c3cc1eeb 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailScreen.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.Share import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api @@ -31,9 +32,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter @@ -183,8 +186,28 @@ private fun GalleryDetailNavigationBar( state: GalleryDetailState, processIntent: (GalleryDetailIntent) -> Unit = {}, ) { - Column { + Column( + modifier = Modifier + .background(color = MaterialTheme.colorScheme.surface), + ) { if (state is GalleryDetailState.Content) { + OutlinedButton( + modifier = Modifier + .padding(horizontal = 24.dp, vertical = 4.dp) + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + onClick = { processIntent(GalleryDetailIntent.Report) }, + ) { + Icon( + modifier = Modifier.padding(end = 8.dp), + imageVector = Icons.Default.Report, + contentDescription = "Report", + ) + Text( + text = stringResource(LocalizationR.string.report_title), + color = LocalContentColor.current + ) + } Row( modifier = Modifier .background(color = MaterialTheme.colorScheme.surface) diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailViewModel.kt index 54ffa569..5b2e2d28 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailViewModel.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/gallery/detail/GalleryDetailViewModel.kt @@ -81,6 +81,10 @@ class GalleryDetailViewModel( ) GalleryDetailIntent.DismissDialog -> setActiveModal(Modal.None) + + GalleryDetailIntent.Report -> (currentState as? GalleryDetailState.Content) + ?.id + ?.let(mainRouter::navigateToReportImage) } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportIntent.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportIntent.kt new file mode 100644 index 00000000..60cb4ed1 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportIntent.kt @@ -0,0 +1,12 @@ +package com.shifthackz.aisdv1.presentation.screen.report + +import com.shifthackz.aisdv1.domain.entity.ReportReason +import com.shifthackz.android.core.mvi.MviIntent + +sealed interface ReportIntent : MviIntent { + data class UpdateText(val text: String) : ReportIntent + data class UpdateReason(val reason: ReportReason) : ReportIntent + data object Submit : ReportIntent + data object NavigateBack : ReportIntent + data object DismissError : ReportIntent +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportScreen.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportScreen.kt new file mode 100644 index 00000000..f98ab6e7 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportScreen.kt @@ -0,0 +1,286 @@ +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) + +package com.shifthackz.aisdv1.presentation.screen.report + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.shifthackz.aisdv1.domain.entity.ReportReason +import com.shifthackz.aisdv1.core.localization.R as LocalizationR +import com.shifthackz.aisdv1.presentation.modal.ModalRenderer +import com.shifthackz.aisdv1.presentation.theme.isSdAppInDarkTheme +import com.shifthackz.aisdv1.presentation.widget.input.chip.ChipTextFieldItem +import com.shifthackz.android.core.mvi.MviComponent + +@Composable +fun ReportScreen( + viewModel: ReportViewModel, + modifier: Modifier = Modifier, +) { + MviComponent( + viewModel = viewModel, + ) { state, intentHandler -> + ReportScreenContent( + modifier = modifier, + state = state, + processIntent = intentHandler, + ) + } +} + +@Composable +@Preview(name = "Loading State") +private fun ReportScreenContent( + modifier: Modifier = Modifier, + state: ReportState = ReportState(), + processIntent: (ReportIntent) -> Unit = {}, +) { + Scaffold( + modifier = modifier.imePadding(), + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = stringResource(LocalizationR.string.report_title), + ) + }, + navigationIcon = { + IconButton( + onClick = { + processIntent(ReportIntent.NavigateBack) + }, + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Back button", + ) + } + }, + ) + }, + bottomBar = { + Button( + modifier = Modifier + .navigationBarsPadding() + .height(height = 60.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp), + onClick = { + val intent = if (state.reportSent) { + ReportIntent.NavigateBack + } else { + ReportIntent.Submit + } + processIntent(intent) + }, + enabled = !state.loading, + ) { + Icon( + modifier = Modifier.size(18.dp), + imageVector = if (state.reportSent) { + Icons.Default.Check + } else { + Icons.AutoMirrored.Filled.Send + }, + contentDescription = "Send", + ) + Text( + modifier = Modifier.padding(start = 8.dp), + text = stringResource( + if (state.reportSent) LocalizationR.string.report_done + else LocalizationR.string.report_submit + ), + color = LocalContentColor.current, + ) + } + }, + ) { paddingValues -> + AnimatedContent( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + targetState = !state.loading, + label = "report_state_animator", + ) { contentVisible -> + if (contentVisible) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (state.reportSent) { + Spacer(modifier = Modifier.weight(1f)) + Icon( + modifier = Modifier.size(100.dp), + imageVector = Icons.Default.CheckCircle, + contentDescription = "Done", + ) + Text( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 16.dp), + text = stringResource(LocalizationR.string.report_sent), + style = MaterialTheme.typography.headlineMedium, + ) + Spacer(modifier = Modifier.weight(1f)) + } else { + state.imageBitmap?.asImageBitmap()?.let { + Image( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .fillMaxWidth(0.45f) + .aspectRatio(1f) + .clip(RoundedCornerShape(12.dp)), + bitmap = it, + contentScale = ContentScale.Crop, + contentDescription = "ai", + ) + } + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + .drawWithContent { drawContent() }, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + ReportReason.entries.forEach { reason -> + val resId: Int = when (reason) { + ReportReason.InappropriateContent -> { + LocalizationR.string.report_reason_inappropriate_content + } + + ReportReason.Violence -> { + LocalizationR.string.report_reason_violence + } + + ReportReason.HatefulSpeech -> { + LocalizationR.string.report_reason_hateful_speech + } + + ReportReason.IntellectualPropertyInfringement -> { + LocalizationR.string.report_reason_intellectual + } + + ReportReason.AdultContent -> { + LocalizationR.string.report_reason_adult + } + + ReportReason.Other -> { + LocalizationR.string.report_reason_other + } + } + val isDark = isSdAppInDarkTheme() + ChipTextFieldItem( + modifier = if (reason == state.reason) { + Modifier.border( + width = 2.dp, + color = if (isDark) Color.White else Color.DarkGray, + shape = RoundedCornerShape(4.dp) + ) + } else { + Modifier + }, + innerPadding = PaddingValues( + vertical = 2.dp, + horizontal = 6.dp + ), + text = stringResource(resId), + onItemClick = { processIntent(ReportIntent.UpdateReason(reason)) }, + ) + } + } + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + value = state.text, + onValueChange = { processIntent(ReportIntent.UpdateText(it)) }, + label = { + Text( + text = stringResource(LocalizationR.string.report_description), + ) + } + ) + } + } + } else { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier + .size(60.dp) + .aspectRatio(1f), + ) + } + } + } + } + ModalRenderer(screenModal = state.screenModal) { + (it as? ReportIntent)?.let(processIntent::invoke) + } +} + +@Composable +@Preview(name = "Content state") +private fun PreviewContent() { + ReportScreenContent( + state = ReportState(loading = false) + ) +} + +@Composable +@Preview(name = "Sent state") +private fun PreviewSent() { + ReportScreenContent( + state = ReportState(loading = false, reportSent = true) + ) +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportState.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportState.kt new file mode 100644 index 00000000..8bc9c111 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportState.kt @@ -0,0 +1,16 @@ +package com.shifthackz.aisdv1.presentation.screen.report + +import android.graphics.Bitmap +import com.shifthackz.aisdv1.domain.entity.ReportReason +import com.shifthackz.aisdv1.presentation.model.Modal +import com.shifthackz.android.core.mvi.MviState + +data class ReportState( + val loading: Boolean = true, + val screenModal: Modal = Modal.None, + val imageBitmap: Bitmap? = null, + val imageBase64: String = "", + val text: String = "", + val reason: ReportReason = ReportReason.Other, + val reportSent: Boolean = false, +) : MviState diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportViewModel.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportViewModel.kt new file mode 100644 index 00000000..bd56d561 --- /dev/null +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/screen/report/ReportViewModel.kt @@ -0,0 +1,103 @@ +package com.shifthackz.aisdv1.presentation.screen.report + +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.imageprocessing.Base64ToBitmapConverter +import com.shifthackz.aisdv1.core.model.asUiText +import com.shifthackz.aisdv1.core.viewmodel.MviRxViewModel +import com.shifthackz.aisdv1.domain.entity.AiGenerationResult +import com.shifthackz.aisdv1.domain.entity.ReportReason +import com.shifthackz.aisdv1.domain.usecase.caching.GetLastResultFromCacheUseCase +import com.shifthackz.aisdv1.domain.usecase.generation.GetGenerationResultUseCase +import com.shifthackz.aisdv1.domain.usecase.report.SendReportUseCase +import com.shifthackz.aisdv1.presentation.model.Modal +import com.shifthackz.aisdv1.presentation.navigation.router.main.MainRouter +import com.shifthackz.android.core.mvi.EmptyEffect +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.kotlin.subscribeBy + +class ReportViewModel( + val itemId: Long, + private val sendReportUseCase: SendReportUseCase, + private val getGenerationResultUseCase: GetGenerationResultUseCase, + private val getLastResultFromCacheUseCase: GetLastResultFromCacheUseCase, + private val base64ToBitmapConverter: Base64ToBitmapConverter, + private val mainRouter: MainRouter, + private val schedulersProvider: SchedulersProvider, +) : MviRxViewModel() { + + override val initialState = ReportState() + + init { + !getGenerationResult(itemId) + .subscribeOnMainThread(schedulersProvider) + .flatMap { ai -> + base64ToBitmapConverter(Base64ToBitmapConverter.Input(ai.image)) + .map(Base64ToBitmapConverter.Output::bitmap) + .map { bitmap -> ai.image to bitmap } + } + .subscribeBy(::errorLog) { (base64, bitmap) -> + updateState { state -> + state.copy( + imageBitmap = bitmap, + imageBase64 = base64, + loading = false, + ) + } + } + } + + override fun processIntent(intent: ReportIntent) { + when (intent) { + ReportIntent.Submit -> !sendReportUseCase( + text = currentState.text, + reason = currentState.reason ?: ReportReason.Other, + image = currentState.imageBase64, + ) + .doOnSubscribe { + updateState { it.copy(loading = true) } + } + .doFinally { + updateState { it.copy(loading = false) } + } + .subscribeOnMainThread(schedulersProvider) + .subscribeBy( + onError = { t -> + errorLog(t) + updateState { + it.copy( + reportSent = false, + screenModal = Modal.Error( + (t.localizedMessage ?: t.message + ?: "Something went wrong").asUiText(), + ) + ) + } + }, + onComplete = { + updateState { it.copy(reportSent = true) } + }, + ) + + is ReportIntent.UpdateReason -> updateState { + it.copy(reason = intent.reason) + } + + is ReportIntent.UpdateText -> updateState { + it.copy(text = intent.text) + } + + ReportIntent.NavigateBack -> mainRouter.navigateBack() + + ReportIntent.DismissError -> updateState { + it.copy(screenModal = Modal.None) + } + } + } + + private fun getGenerationResult(id: Long): Single { + if (id <= 0) return getLastResultFromCacheUseCase() + return getGenerationResultUseCase(id) + } +} diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/dialog/GenerationImageResultDialog.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/dialog/GenerationImageResultDialog.kt index c467964e..3201d632 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/dialog/GenerationImageResultDialog.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/dialog/GenerationImageResultDialog.kt @@ -49,6 +49,7 @@ fun GenerationImageResultDialog( showSaveButton: Boolean = false, onDismissRequest: () -> Unit = {}, onSaveRequest: () -> Unit = {}, + onReportRequest: () -> Unit = {}, onViewDetailRequest: () -> Unit = {}, ) { Dialog(onDismissRequest = onDismissRequest) { @@ -65,7 +66,7 @@ fun GenerationImageResultDialog( Image( modifier = Modifier .fillMaxWidth() - .defaultMinSize(minHeight = 300.dp,) + .defaultMinSize(minHeight = 300.dp) .align(Alignment.CenterHorizontally) .clickable( interactionSource = remember { MutableInteractionSource() }, @@ -88,32 +89,30 @@ fun GenerationImageResultDialog( color = LocalContentColor.current, ) } - OutlinedButton( - modifier = Modifier - .padding(top = 8.dp) - .align(Alignment.CenterHorizontally) - .fillMaxWidth(0.7f), - onClick = onDismissRequest, - ) { - Text( - text = stringResource(id = LocalizationR.string.action_close), - color = LocalContentColor.current, - ) - } - - } else { - Button( - modifier = Modifier - .padding(top = 16.dp) - .align(Alignment.CenterHorizontally) - .fillMaxWidth(0.7f), - onClick = onDismissRequest, - ) { - Text( - text = stringResource(id = LocalizationR.string.action_close), - color = LocalContentColor.current, - ) - } + } + Button( + modifier = Modifier + .padding(top = 16.dp) + .align(Alignment.CenterHorizontally) + .fillMaxWidth(0.7f), + onClick = onReportRequest, + ) { + Text( + text = stringResource(id = LocalizationR.string.report_title), + color = LocalContentColor.current, + ) + } + OutlinedButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.CenterHorizontally) + .fillMaxWidth(0.7f), + onClick = onDismissRequest, + ) { + Text( + text = stringResource(id = LocalizationR.string.action_close), + color = LocalContentColor.current, + ) } } } diff --git a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/input/chip/ChipTextFieldItem.kt b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/input/chip/ChipTextFieldItem.kt index 99cc0917..7ffade3b 100644 --- a/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/input/chip/ChipTextFieldItem.kt +++ b/presentation/src/main/java/com/shifthackz/aisdv1/presentation/widget/input/chip/ChipTextFieldItem.kt @@ -2,6 +2,7 @@ package com.shifthackz.aisdv1.presentation.widget.input.chip import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -14,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.shifthackz.aisdv1.presentation.model.ExtraType @@ -26,6 +28,8 @@ fun ChipTextFieldItem( type: ExtraType? = null, text: String, overflow: TextOverflow = TextOverflow.Clip, + shape: Shape = RoundedCornerShape(4.dp), + innerPadding: PaddingValues = PaddingValues(vertical = 1.dp, horizontal = 2.dp), maxLines: Int = Int.MAX_VALUE, showDeleteIcon: Boolean = false, onDeleteClick: () -> Unit = {}, @@ -47,10 +51,10 @@ fun ChipTextFieldItem( } Row( modifier = modifier - .clip(RoundedCornerShape(4.dp)) + .clip(shape) .background(bgColor) .clickable { onItemClick() } - .padding(vertical = 1.dp, horizontal = 2.dp), + .padding(innerPadding), verticalAlignment = Alignment.CenterVertically, ) { val localColor = MaterialTheme.colorScheme.onPrimary