From b3466ffbf3e746cfc5d91e85ba68a2b2563f0fca Mon Sep 17 00:00:00 2001 From: DatLag Date: Tue, 16 Apr 2024 22:46:59 +0200 Subject: [PATCH] added image scan --- .fleet/settings.json | 16 ++++ composeApp/build.gradle.kts | 1 + .../aniflow/other/ImagePickerState.android.kt | 41 +++++++++ .../component/TranslateButton.android.kt | 5 -- .../datlag/aniflow/module/NetworkModule.kt | 21 +++++ .../datlag/aniflow/other/ImagePickerState.kt | 12 +++ .../screen/initial/component/CompactScreen.kt | 32 +++---- .../initial/component/ExpandedScreen.kt | 32 +++---- .../screen/initial/component/MediumScreen.kt | 32 +++---- .../screen/initial/home/HomeComponent.kt | 4 + .../screen/initial/home/HomeScreen.kt | 13 ++- .../initial/home/HomeScreenComponent.kt | 16 ++++ .../screen/initial/model/FABConfig.kt | 1 + .../aniflow/other/ImagePickerState.ios.kt | 15 ++++ settings.gradle.kts | 1 + trace/build.gradle.kts | 54 ++++++++++++ .../kotlin/dev/datlag/aniflow/trace/Trace.kt | 17 ++++ .../datlag/aniflow/trace/TraceStateMachine.kt | 86 +++++++++++++++++++ .../aniflow/trace/model/SearchResponse.kt | 36 ++++++++ 19 files changed, 383 insertions(+), 52 deletions(-) create mode 100644 .fleet/settings.json create mode 100644 composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.android.kt create mode 100644 composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.kt create mode 100644 composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.ios.kt create mode 100644 trace/build.gradle.kts create mode 100644 trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/Trace.kt create mode 100644 trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt create mode 100644 trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt diff --git a/.fleet/settings.json b/.fleet/settings.json new file mode 100644 index 0000000..8dcacc6 --- /dev/null +++ b/.fleet/settings.json @@ -0,0 +1,16 @@ +{ + "plugins": [ + { + "type": "add", + "pluginName": "fleet.gradle" + }, + { + "type": "add", + "pluginName": "fleet.kotlin" + }, + { + "type": "add", + "pluginName": "fleet.mercury" + } + ] +} \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 449cd39..e0a4209 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -115,6 +115,7 @@ kotlin { implementation(project(":anilist")) implementation(project(":model")) implementation(project(":settings")) + implementation(project(":trace")) } iosMain.dependencies { diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.android.kt new file mode 100644 index 0000000..313d6af --- /dev/null +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.android.kt @@ -0,0 +1,41 @@ +package dev.datlag.aniflow.other + +import android.net.Uri +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext + +actual data class ImagePickerState( + private val mediaPicker: ManagedActivityResultLauncher +) { + actual fun launch() { + mediaPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } +} + +@Composable +actual fun rememberImagePickerState(onPick: (ByteArray?) -> Unit): ImagePickerState { + val context = LocalContext.current + val mediaPicker = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia(), + onResult = { uri -> + onPick( + uri?.let { + context.contentResolver.openInputStream(it)?.use { input -> + input.readBytes() + } + } + ) + } + ) + + return remember(mediaPicker) { + ImagePickerState( + mediaPicker + ) + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt index 275342d..adb5582 100644 --- a/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt +++ b/composeApp/src/androidMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/TranslateButton.android.kt @@ -14,7 +14,6 @@ import com.google.mlkit.nl.translate.Translation import com.google.mlkit.nl.translate.TranslatorOptions import dev.datlag.aniflow.SharedRes import dev.icerock.moko.resources.compose.stringResource -import io.github.aakira.napier.Napier import java.util.Locale @Composable @@ -24,15 +23,12 @@ actual fun TranslateButton( ) { val locale = remember { Locale.getDefault() } if (locale.language.equals(Locale.forLanguageTag("en").language, ignoreCase = true)) { - Napier.e("Language is english") return } if (locale.toLanguageTag().equals("en", ignoreCase = true)) { - Napier.e("LanguageTag is english") return } if (locale.isO3Language.equals("ENG", ignoreCase = true)) { - Napier.e("Language ISO is english") return } @@ -43,7 +39,6 @@ actual fun TranslateButton( } if (targetLanguage == null || targetLanguage == TranslateLanguage.ENGLISH) { - Napier.e("TargetLanguage is: $targetLanguage, ${locale.toLanguageTag()}") return } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt index 4e650d2..fa992ca 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/module/NetworkModule.kt @@ -12,6 +12,8 @@ import com.apollographql.apollo3.api.http.HttpRequest import com.apollographql.apollo3.api.http.HttpResponse import com.apollographql.apollo3.network.http.HttpInterceptor import com.apollographql.apollo3.network.http.HttpInterceptorChain +import de.jensklingenberg.ktorfit.Ktorfit +import de.jensklingenberg.ktorfit.ktorfitBuilder import dev.datlag.aniflow.BuildKonfig import dev.datlag.aniflow.Sekret import dev.datlag.aniflow.anilist.AiringTodayStateMachine @@ -27,6 +29,8 @@ import org.kodein.di.bindSingleton import org.kodein.di.instance import dev.datlag.aniflow.common.nullableFirebaseInstance import dev.datlag.aniflow.other.TokenRefreshHandler +import dev.datlag.aniflow.trace.Trace +import dev.datlag.aniflow.trace.TraceStateMachine import dev.datlag.tooling.async.suspendCatching import io.github.aakira.napier.Napier import org.kodein.di.bindProvider @@ -135,5 +139,22 @@ data object NetworkModule { bindSingleton { TokenRefreshHandler(instance()) } + bindSingleton { + ktorfitBuilder { + httpClient(instance()) + } + } + bindSingleton { + val builder = instance() + builder.build { + baseUrl("https://api.trace.moe/") + }.create() + } + bindProvider { + TraceStateMachine( + trace = instance(), + crashlytics = nullableFirebaseInstance()?.crashlytics + ) + } } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.kt new file mode 100644 index 0000000..a6fa78b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.kt @@ -0,0 +1,12 @@ +package dev.datlag.aniflow.other + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import io.ktor.utils.io.* + +expect class ImagePickerState { + fun launch() +} + +@Composable +expect fun rememberImagePickerState(onPick: (ByteArray?) -> Unit = { }): ImagePickerState \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt index a3c25fd..4a5e5cb 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/CompactScreen.kt @@ -61,21 +61,23 @@ fun CompactScreen(component: InitialComponent) { when (val current = state) { is FABConfig.Scan -> { - ExtendedFloatingActionButton( - onClick = current.onClick, - icon = { - Icon( - imageVector = Icons.Filled.CameraEnhance, - contentDescription = null - ) - }, - text = { - Text( - text = "Scan" - ) - }, - expanded = current.listState.isScrollingUp() - ) + if (!current.loading) { + ExtendedFloatingActionButton( + onClick = current.onClick, + icon = { + Icon( + imageVector = Icons.Filled.CameraEnhance, + contentDescription = null + ) + }, + text = { + Text( + text = "Scan" + ) + }, + expanded = current.listState.isScrollingUp(), + ) + } } else -> { } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt index 4960d32..813963a 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/ExpandedScreen.kt @@ -27,21 +27,23 @@ fun ExpandedScreen(component: InitialComponent) { when (val current = state) { is FABConfig.Scan -> { - ExtendedFloatingActionButton( - onClick = current.onClick, - icon = { - Icon( - imageVector = Icons.Filled.CameraEnhance, - contentDescription = null - ) - }, - text = { - Text( - text = "Scan" - ) - }, - expanded = current.listState.isScrollingUp() - ) + if (!current.loading) { + ExtendedFloatingActionButton( + onClick = current.onClick, + icon = { + Icon( + imageVector = Icons.Filled.CameraEnhance, + contentDescription = null + ) + }, + text = { + Text( + text = "Scan" + ) + }, + expanded = current.listState.isScrollingUp(), + ) + } } else -> { } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt index e10514c..2134bd1 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/component/MediumScreen.kt @@ -26,21 +26,23 @@ fun MediumScreen(component: InitialComponent) { when (val current = state) { is FABConfig.Scan -> { - ExtendedFloatingActionButton( - onClick = current.onClick, - icon = { - Icon( - imageVector = Icons.Filled.CameraEnhance, - contentDescription = null - ) - }, - text = { - Text( - text = "Scan" - ) - }, - expanded = current.listState.isScrollingUp() - ) + if (!current.loading) { + ExtendedFloatingActionButton( + onClick = current.onClick, + icon = { + Icon( + imageVector = Icons.Filled.CameraEnhance, + contentDescription = null + ) + }, + text = { + Text( + text = "Scan" + ) + }, + expanded = current.listState.isScrollingUp(), + ) + } } else -> { } } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt index 690bf8b..960fafe 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeComponent.kt @@ -7,8 +7,10 @@ import dev.datlag.aniflow.anilist.PopularSeasonStateMachine import dev.datlag.aniflow.anilist.TrendingAnimeStateMachine import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.SeasonState +import dev.datlag.aniflow.trace.TraceStateMachine import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.ContentHolderComponent +import io.ktor.utils.io.* import kotlinx.coroutines.flow.StateFlow interface HomeComponent : ContentHolderComponent { @@ -16,6 +18,8 @@ interface HomeComponent : ContentHolderComponent { val trendingState: StateFlow val popularSeasonState: StateFlow val popularNextSeasonState: StateFlow + val traceState: StateFlow fun details(medium: Medium) + fun trace(channel: ByteArray) } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt index 9b89acd..74ca3f5 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreen.kt @@ -27,10 +27,12 @@ import dev.datlag.aniflow.LocalPaddingValues import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.common.plus import dev.datlag.aniflow.other.StateSaver +import dev.datlag.aniflow.other.rememberImagePickerState import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.AiringOverview import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.PopularSeasonOverview import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.TrendingOverview import dev.datlag.aniflow.ui.navigation.screen.initial.model.FABConfig +import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle @Composable fun HomeScreen(component: HomeComponent) { @@ -44,11 +46,18 @@ private fun MainView(component: HomeComponent, modifier: Modifier = Modifier) { initialFirstVisibleItemIndex = StateSaver.List.homeOverview, initialFirstVisibleItemScrollOffset = StateSaver.List.homeOverviewOffset ) + val imagePicker = rememberImagePickerState { + it?.let(component::trace) + } + val traceState by component.traceState.collectAsStateWithLifecycle() - LaunchedEffect(listState) { + LaunchedEffect(listState, traceState) { FABConfig.state.value = FABConfig.Scan( listState = listState, - onClick = { } + loading = traceState.isLoading, + onClick = { + imagePicker.launch() + } ) } diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt index 8cea71f..2d8509a 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/HomeScreenComponent.kt @@ -11,6 +11,7 @@ import dev.datlag.aniflow.anilist.TrendingAnimeStateMachine import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.state.SeasonState import dev.datlag.aniflow.common.onRender +import dev.datlag.aniflow.trace.TraceStateMachine import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.navigation.screen.medium.MediumScreenComponent import dev.datlag.tooling.compose.ioDispatcher @@ -64,6 +65,15 @@ class HomeScreenComponent( initialValue = PopularNextSeasonStateMachine.currentState ) + private val traceStateMachine by di.instance() + override val traceState: StateFlow = traceStateMachine.state.flowOn( + context = ioDispatcher() + ).stateIn( + scope = ioScope(), + started = SharingStarted.WhileSubscribed(), + initialValue = TraceStateMachine.State.Waiting + ) + @Composable override fun render() { onRender { @@ -78,4 +88,10 @@ class HomeScreenComponent( override fun details(medium: Medium) { onMediumDetails(medium) } + + override fun trace(channel: ByteArray) { + launchIO { + traceStateMachine.dispatch(TraceStateMachine.Action.Load(channel)) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt index edd35cc..7f79510 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/model/FABConfig.kt @@ -7,6 +7,7 @@ sealed interface FABConfig { data class Scan( val listState: LazyListState, + val loading: Boolean = false, val onClick: () -> Unit ) : FABConfig diff --git a/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.ios.kt b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.ios.kt new file mode 100644 index 0000000..bc64dc5 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/dev/datlag/aniflow/other/ImagePickerState.ios.kt @@ -0,0 +1,15 @@ +package dev.datlag.aniflow.other + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import io.ktor.utils.io.* + +actual object ImagePickerState { + actual fun launch() { + } +} + +@Composable +actual fun rememberImagePickerState(onPick: (ByteArray?) -> Unit): ImagePickerState { + return ImagePickerState +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a59f072..bfd2268 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ include(":firebase") include(":anilist") include(":model") include(":settings") +include(":trace") pluginManagement { repositories { diff --git a/trace/build.gradle.kts b/trace/build.gradle.kts new file mode 100644 index 0000000..b1b11b3 --- /dev/null +++ b/trace/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + alias(libs.plugins.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.serialization) + alias(libs.plugins.ksp) +} + +val artifact = VersionCatalog.artifactName("trace") + +kotlin { + androidTarget() + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + applyDefaultHierarchyTemplate() + + sourceSets { + commonMain.dependencies { + api(libs.coroutines) + implementation(libs.tooling) + api(libs.ktorfit) + implementation(libs.serialization) + api(libs.flowredux) + + api(project(":model")) + implementation(project(":firebase")) + } + } +} + +dependencies { + add("kspCommonMainMetadata", libs.ktorfit.ksp) + add("kspAndroid", libs.ktorfit.ksp) + add("kspJvm", libs.ktorfit.ksp) + add("kspIosX64", libs.ktorfit.ksp) + add("kspIosArm64", libs.ktorfit.ksp) + add("kspIosSimulatorArm64", libs.ktorfit.ksp) +} + +android { + compileSdk = Configuration.compileSdk + namespace = artifact + + defaultConfig { + minSdk = Configuration.minSdk + } + compileOptions { + sourceCompatibility = CompileOptions.sourceCompatibility + targetCompatibility = CompileOptions.targetCompatibility + } +} diff --git a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/Trace.kt b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/Trace.kt new file mode 100644 index 0000000..9ad72d6 --- /dev/null +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/Trace.kt @@ -0,0 +1,17 @@ +package dev.datlag.aniflow.trace + +import de.jensklingenberg.ktorfit.http.Body +import de.jensklingenberg.ktorfit.http.POST +import de.jensklingenberg.ktorfit.http.Query +import dev.datlag.aniflow.trace.model.SearchResponse +import io.ktor.client.request.forms.* +import io.ktor.utils.io.* + +interface Trace { + + @POST("search") + suspend fun search( + @Body image: ByteArray, + @Query("cutBorders") cutBorders: Boolean? = true + ): SearchResponse +} \ No newline at end of file diff --git a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt new file mode 100644 index 0000000..2c22244 --- /dev/null +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt @@ -0,0 +1,86 @@ +package dev.datlag.aniflow.trace + +import com.freeletics.flowredux.dsl.FlowReduxStateMachine +import dev.datlag.aniflow.firebase.FirebaseFactory +import dev.datlag.aniflow.model.CatchResult +import dev.datlag.aniflow.trace.model.SearchResponse +import io.ktor.client.request.forms.* +import io.ktor.utils.io.* +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@OptIn(ExperimentalCoroutinesApi::class) +class TraceStateMachine( + private val trace: Trace, + private val crashlytics: FirebaseFactory.Crashlytics? +) : FlowReduxStateMachine( + initialState = State.Waiting +) { + + init { + spec { + inState { + on { action, state -> + state.override { State.Loading(action.image) } + } + } + inState { + onEnter { state -> + val response = CatchResult.repeat(2) { + val result = trace.search(state.snapshot.image) + + if (result.isError) { + throw IllegalStateException("Result is Error") + } else { + result + } + }.mapSuccess { + State.Success(it) + } + + state.override { + response.asSuccess { + crashlytics?.log(it) + + State.Error + } + } + } + } + inState { + on { action, state -> + state.override { State.Loading(action.image) } + } + } + inState { + on { action, state -> + state.override { State.Loading(action.image) } + } + } + } + } + + sealed interface State { + + val isWaiting: Boolean + get() = this is Waiting + + val isLoading: Boolean + get() = this is Loading + + data object Waiting : State + + class Loading( + internal val image: ByteArray + ) : State + + data class Success( + val response: SearchResponse + ) : State + + data object Error : State + } + + sealed interface Action { + class Load(internal val image: ByteArray) : Action + } +} \ No newline at end of file diff --git a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt new file mode 100644 index 0000000..158befd --- /dev/null +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt @@ -0,0 +1,36 @@ +package dev.datlag.aniflow.trace.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +data class SearchResponse( + @SerialName("error") val error: String? = null, + @SerialName("result") val result: List = emptyList() +) { + + @Transient + val isError = !error.isNullOrBlank() + + @Transient + private val onlyHighResults: List = result.filter { + it.similarity >= 0.9F + } + + @Transient + private val groupedResults: Map> = onlyHighResults.groupBy { + it.aniList + } + + @Transient + val bestResult: Result? = groupedResults.maxByOrNull { + it.value.map { v -> v.similarity }.average() + }?.value?.maxByOrNull { it.similarity } + + @Serializable + data class Result( + @SerialName("anilist") val aniList: Int, + @SerialName("similarity") val similarity: Float = 0F, + ) +}