diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index e0a4209..e7f365b 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -110,6 +110,7 @@ kotlin { implementation(libs.oidc) implementation("dev.datlag.sheets-compose-dialogs:rating:2.0.0-SNAPSHOT") + // implementation("dev.datlag.sheets-compose-dialogs:option:2.0.0-SNAPSHOT") implementation(project(":firebase")) implementation(project(":anilist")) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt index 887f82f..99ab95f 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendMedium.kt @@ -6,6 +6,7 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.anilist.type.MediaFormat import dev.datlag.aniflow.anilist.type.MediaRankType import dev.datlag.aniflow.anilist.type.MediaStatus +import dev.datlag.aniflow.trace.model.SearchResponse import dev.icerock.moko.resources.StringResource fun Medium.preferred(): String { @@ -122,3 +123,33 @@ fun Medium.Full.popular(): Medium.Ranking? = this.ranking.popular() expect fun Medium.Character.Name.preferred(): String fun Medium.Character.preferredName(): String = this.name.preferred() + +private fun SearchResponse.Result.AniList.Title?.asMediumTitle(): Medium.Title { + return Medium.Title( + native = this?.native, + english = this?.english, + romaji = this?.romaji, + userPreferred = null + ) +} + +fun SearchResponse.Result.AniList.asMedium(): Medium { + return Medium( + id = this.id, + idMal = this.idMal, + isAdult = this.isAdult, + genres = emptySet(), + bannerImage = null, + coverImage = Medium.CoverImage( + color = null, + medium = null, + large = null, + extraLarge = null + ), + countryOfOrigin = null, + averageScore = -1, + title = this.title.asMediumTitle() + ) +} + +fun SearchResponse.Result.AniList.Title.preferred(): String = this.asMediumTitle().preferred() \ 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 74ca3f5..0f671e3 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 @@ -13,32 +13,35 @@ import androidx.compose.material3.* import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import dev.chrisbanes.haze.haze import dev.datlag.aniflow.LocalHaze import dev.datlag.aniflow.LocalPaddingValues +import dev.datlag.aniflow.common.asMedium import dev.datlag.aniflow.common.isScrollingUp import dev.datlag.aniflow.common.plus +import dev.datlag.aniflow.common.preferred import dev.datlag.aniflow.other.StateSaver import dev.datlag.aniflow.other.rememberImagePickerState +import dev.datlag.aniflow.trace.TraceStateMachine 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 +import io.github.aakira.napier.Napier @Composable fun HomeScreen(component: HomeComponent) { MainView(component, Modifier.fillMaxWidth()) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainView(component: HomeComponent, modifier: Modifier = Modifier) { val padding = PaddingValues(vertical = 16.dp) @@ -61,6 +64,33 @@ private fun MainView(component: HomeComponent, modifier: Modifier = Modifier) { ) } + when (val current = traceState) { + is TraceStateMachine.State.Success -> { + val results = remember(traceState) { current.response.result.sortedByDescending { it.similarity } } + + SideEffect { + results.maxByOrNull { it.similarity }?.aniList?.asMedium()?.let(component::details) + } + /*OptionDialog( + state = rememberUseCaseState(visible = true), + config = OptionConfig(mode = DisplayMode.LIST), + selection = OptionSelection.Single( + options = results.mapIndexedNotNull { index, result -> + result.aniList.title?.preferred()?.let { title -> + Option( + titleText = title, + selected = index == 0 + ) + } + } + ) { index, _ -> + results.getOrNull(index)?.aniList?.asMedium()?.let(component::details) + } + )*/ + } + else -> { } + } + LazyColumn( state = listState, modifier = modifier.haze(state = LocalHaze.current), diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt index 46a6222..9ea69d6 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/MediumScreen.kt @@ -57,6 +57,7 @@ import dev.datlag.aniflow.ui.custom.EditFAB import dev.datlag.aniflow.ui.navigation.screen.initial.home.component.GenreChip import dev.datlag.aniflow.ui.navigation.screen.medium.component.CharacterCard import dev.datlag.aniflow.ui.navigation.screen.medium.component.TranslateButton +import dev.datlag.tooling.compose.ifTrue import dev.datlag.tooling.compose.onClick import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import dev.icerock.moko.resources.compose.painterResource @@ -247,15 +248,19 @@ fun MediumScreen(component: MediumComponent) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp) ) { + var coverShadow by remember(coverImage) { mutableStateOf(false) } + AsyncImage( modifier = Modifier .width(140.dp) .height(200.dp) - .shadow( - elevation = 8.dp, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary - ), + .ifTrue(coverShadow) { + shadow( + elevation = 8.dp, + shape = MaterialTheme.shapes.medium, + spotColor = MaterialTheme.colorScheme.primary + ) + }, model = coverImage.extraLarge, contentScale = ContentScale.Crop, contentDescription = null, @@ -267,9 +272,18 @@ fun MediumScreen(component: MediumComponent) { error = rememberAsyncImagePainter( model = coverImage.medium, contentScale = ContentScale.Crop, - placeholder = shimmerPainter() - ) - ) + placeholder = shimmerPainter(), + onSuccess = { + coverShadow = true + } + ), + onSuccess = { + coverShadow = true + } + ), + onSuccess = { + coverShadow = true + } ) Column( modifier = Modifier.weight(1F).fillMaxHeight(), diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterCard.kt index 419dd6b..b227716 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/medium/component/CharacterCard.kt @@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -21,6 +21,7 @@ import coil3.compose.rememberAsyncImagePainter import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.preferredName import dev.datlag.aniflow.common.shimmerPainter +import dev.datlag.tooling.compose.ifTrue @Composable fun CharacterCard( @@ -34,24 +35,34 @@ fun CharacterCard( onClick(char) } ) { + var imageShadow by remember(char.image) { mutableStateOf(false) } + AsyncImage( model = char.image.large, contentDescription = null, modifier = Modifier .fillMaxWidth() .aspectRatio(0.7F) - .shadow( - elevation = 8.dp, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary - ), + .ifTrue(imageShadow) { + shadow( + elevation = 8.dp, + shape = MaterialTheme.shapes.medium, + spotColor = MaterialTheme.colorScheme.primary + ) + }, contentScale = ContentScale.Crop, placeholder = shimmerPainter(), error = rememberAsyncImagePainter( model = char.image.medium, contentScale = ContentScale.Crop, - placeholder = shimmerPainter() - ) + placeholder = shimmerPainter(), + onSuccess = { + imageShadow = true + } + ), + onSuccess = { + imageShadow = true + } ) Box( diff --git a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/Trace.kt b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/Trace.kt index 9ad72d6..9eac930 100644 --- a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/Trace.kt +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/Trace.kt @@ -12,6 +12,7 @@ interface Trace { @POST("search") suspend fun search( @Body image: ByteArray, - @Query("cutBorders") cutBorders: Boolean? = true + @Query("cutBorders") cutBorders: Boolean? = true, + @Query("anilistInfo") anilistInfo: 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 index 2c22244..09e8c5b 100644 --- a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/TraceStateMachine.kt @@ -67,6 +67,9 @@ class TraceStateMachine( val isLoading: Boolean get() = this is Loading + val isSuccess: Boolean + get() = this is Success + data object Waiting : State class Loading( 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 index 7690fab..fd229f9 100644 --- a/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt +++ b/trace/src/commonMain/kotlin/dev/datlag/aniflow/trace/model/SearchResponse.kt @@ -1,8 +1,6 @@ package dev.datlag.aniflow.trace.model -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient +import kotlinx.serialization.* @Serializable data class SearchResponse( @@ -14,23 +12,28 @@ data class SearchResponse( val isError = !error.isNullOrBlank() @Transient - private val onlyHighResults: List = result.filter { - it.similarity >= 0.7F - } - - @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 } + val bestResult: Result? = result.maxByOrNull { it.similarity } @Serializable data class Result( - @SerialName("anilist") val aniList: Int, + @SerialName("anilist") val aniList: AniList, @SerialName("similarity") val similarity: Float = 0F, - ) + ) { + + @Serializable + data class AniList( + @SerialName("id") val id: Int, + @SerialName("idMal") val idMal: Int?, + @SerialName("isAdult") val isAdult: Boolean = false, + @SerialName("title") val title: Title? = null, + ) { + + @Serializable + data class Title( + @SerialName("native") val native: String? = null, + @SerialName("romaji") val romaji: String? = null, + @SerialName("english") val english: String? = null, + ) + } + } }