diff --git a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/media/Media.kt b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/media/Media.kt index ccb9ddf4..050ca5bd 100644 --- a/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/media/Media.kt +++ b/api/anilist/src/main/kotlin/com/imashnake/animite/api/anilist/sanitize/media/Media.kt @@ -5,6 +5,10 @@ import com.imashnake.animite.api.anilist.MediaListQuery import com.imashnake.animite.api.anilist.MediaQuery import com.imashnake.animite.api.anilist.type.MediaRankType +private const val HQ_DEFAULT = "hqdefault" +private const val MAX_RES_DEFAULT = "maxresdefault" +private const val SD_DEFAULT = "sddefault" + data class Media( /** @see MediaQuery.Media.bannerImage */ val bannerImage: String?, @@ -54,7 +58,7 @@ data class Media( * @see MediaQuery.Trailer.site */ val url: String?, /** @see MediaQuery.Trailer.thumbnail */ - val thumbnail: String?, + val thumbnail: Thumbnail, ) { /** @see MediaQuery.Trailer.thumbnail */ enum class Site(val baseUrl: String) { @@ -62,6 +66,12 @@ data class Media( DAILYMOTION("https://www.dailymotion.com/video/"), UNKNOWN("") } + + data class Thumbnail( + val maxResDefault: String?, + val sdDefault: String?, + val defaultThumbnail: String? + ) } internal constructor(query: MediaQuery.Media) : this( @@ -101,7 +111,17 @@ data class Media( } else { Trailer( url = "${Trailer.Site.valueOf(query.trailer.site.uppercase()).baseUrl}${query.trailer.id}", - thumbnail = query.trailer.thumbnail + thumbnail = with(query.trailer) { + Trailer.Thumbnail( + maxResDefault = thumbnail?.takeIf { + it.contains(HQ_DEFAULT) + }?.replace(HQ_DEFAULT, MAX_RES_DEFAULT), + sdDefault = thumbnail?.takeIf { + it.contains(HQ_DEFAULT) + }?.replace(HQ_DEFAULT, SD_DEFAULT), + defaultThumbnail = thumbnail + ) + } ) } ) diff --git a/app/src/main/java/com/imashnake/animite/features/media/MediaPage.kt b/app/src/main/java/com/imashnake/animite/features/media/MediaPage.kt index 83f2b692..c6549f17 100644 --- a/app/src/main/java/com/imashnake/animite/features/media/MediaPage.kt +++ b/app/src/main/java/com/imashnake/animite/features/media/MediaPage.kt @@ -43,7 +43,11 @@ import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -408,16 +412,32 @@ fun MediaTrailer( modifier = Modifier .wrapContentSize() .clip(RoundedCornerShape(dimensionResource(R.dimen.trailer_corner_radius))) + .background(color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.15f)) .clickable { val appIntent = Intent(Intent.ACTION_VIEW, Uri.parse(trailer.url)) context.startActivity(appIntent) } ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(trailer.thumbnail) + var bestThumbnail by rememberSaveable { mutableStateOf(trailer.thumbnail.maxResDefault) } + + val model = remember(bestThumbnail) { + ImageRequest.Builder(context) + .data(bestThumbnail) + .apply { + listener( + onError = { _, _ -> + bestThumbnail = if (bestThumbnail?.contains("maxresdefault") == true) { + trailer.thumbnail.sdDefault + } else trailer.thumbnail.defaultThumbnail + } + ) + } .crossfade(Constants.CROSSFADE_DURATION) - .build(), + .build() + } + + AsyncImage( + model = model, contentDescription = stringResource(R.string.trailer), contentScale = ContentScale.FillWidth, modifier = Modifier