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