Cover's styling: color & loading animation (#42)

* use dominantCoverColors as background for cover so it's less tinted by theme color, also if CoverCache is corrupted, favorite entries can still show color

* add loading animation while cover is fetching image

* Bulk-selection using default isSelected style

* fix cover's background & tint when AsyncImage is Error/Fallback/PlaceHolder

* Widget's cover color when unable to load cover image
This commit is contained in:
Cuong M. Tran 2024-06-04 13:59:00 +07:00 committed by GitHub
parent b6b7114c29
commit 4c713f118f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 290 additions and 31 deletions

View file

@ -99,10 +99,8 @@ internal fun BrowseSourceComfortableGridItem(
ogUrl = manga.thumbnailUrl, ogUrl = manga.thumbnailUrl,
lastModified = manga.coverLastModified, lastModified = manga.coverLastModified,
), ),
isSelected = isSelected,
coverAlpha = when { coverAlpha = when {
// KMK -->
isSelected -> CommonMangaItemDefaults.BrowseSelectedCoverAlpha
// KMK <--
manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha
else -> 1f else -> 1f
}, },

View file

@ -99,10 +99,8 @@ internal fun BrowseSourceCompactGridItem(
ogUrl = manga.thumbnailUrl, ogUrl = manga.thumbnailUrl,
lastModified = manga.coverLastModified, lastModified = manga.coverLastModified,
), ),
isSelected = isSelected,
coverAlpha = when { coverAlpha = when {
// KMK -->
isSelected -> CommonMangaItemDefaults.BrowseSelectedCoverAlpha
// KMK <--
manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha
else -> 1f else -> 1f
}, },

View file

@ -92,10 +92,8 @@ internal fun BrowseSourceListItem(
ogUrl = manga.thumbnailUrl, ogUrl = manga.thumbnailUrl,
lastModified = manga.coverLastModified, lastModified = manga.coverLastModified,
), ),
isSelected = isSelected,
coverAlpha = when { coverAlpha = when {
// KMK -->
isSelected -> CommonMangaItemDefaults.BrowseSelectedCoverAlpha
// KMK <--
manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha manga.favorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha
else -> 1f else -> 1f
}, },

View file

@ -78,10 +78,8 @@ internal fun MangaItem(
coverBadgeStart = { coverBadgeStart = {
InLibraryBadge(enabled = isFavorite) InLibraryBadge(enabled = isFavorite)
}, },
isSelected = isSelected,
coverAlpha = when { coverAlpha = when {
// KMK -->
isSelected -> CommonMangaItemDefaults.BrowseSelectedCoverAlpha
// KMK <--
isFavorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha isFavorite -> CommonMangaItemDefaults.BrowseFavoriteCoverAlpha
else -> 1f else -> 1f
}, },

View file

@ -33,25 +33,24 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import eu.kanade.presentation.manga.components.CoverPlaceholderColor
import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.manga.components.MangaCover
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.BadgeGroup import tachiyomi.presentation.core.components.BadgeGroup
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.selectedBackground import tachiyomi.presentation.core.util.selectedBackground
import tachiyomi.domain.manga.model.MangaCover as DomainMangaCover
object CommonMangaItemDefaults { object CommonMangaItemDefaults {
val GridHorizontalSpacer = 4.dp val GridHorizontalSpacer = 4.dp
val GridVerticalSpacer = 4.dp val GridVerticalSpacer = 4.dp
const val BrowseFavoriteCoverAlpha = 0.34f const val BrowseFavoriteCoverAlpha = 0.34f
// KMK -->
const val BrowseSelectedCoverAlpha = 0.17f
// KMK <--
} }
private val ContinueReadingButtonSize = 28.dp private val ContinueReadingButtonSize = 28.dp
@ -66,7 +65,7 @@ private const val GridSelectedCoverAlpha = 0.76f
*/ */
@Composable @Composable
fun MangaCompactGridItem( fun MangaCompactGridItem(
coverData: tachiyomi.domain.manga.model.MangaCover, coverData: DomainMangaCover,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
isSelected: Boolean = false, isSelected: Boolean = false,
@ -86,8 +85,14 @@ fun MangaCompactGridItem(
MangaCover.Book( MangaCover.Book(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), .background(
color = DomainMangaCover.coverColorMap[coverData.mangaId]?.first?.let { Color(it) }
?: MaterialTheme.colorScheme.surface
),
alpha = if (isSelected) GridSelectedCoverAlpha else coverAlpha,
data = coverData, data = coverData,
tint = DomainMangaCover.coverColorMap[coverData.mangaId]?.second
?: MaterialTheme.colorScheme.onSurface.toArgb(),
) )
}, },
badgesStart = coverBadgeStart, badgesStart = coverBadgeStart,
@ -167,7 +172,7 @@ private fun BoxScope.CoverTextOverlay(
*/ */
@Composable @Composable
fun MangaComfortableGridItem( fun MangaComfortableGridItem(
coverData: tachiyomi.domain.manga.model.MangaCover, coverData: DomainMangaCover,
title: String, title: String,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
@ -189,8 +194,14 @@ fun MangaComfortableGridItem(
MangaCover.Book( MangaCover.Book(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha), .background(
color = DomainMangaCover.coverColorMap[coverData.mangaId]?.first?.let { Color(it) }
?: MaterialTheme.colorScheme.surface
),
alpha = if (isSelected) GridSelectedCoverAlpha else coverAlpha,
data = coverData, data = coverData,
tint = DomainMangaCover.coverColorMap[coverData.mangaId]?.second
?: MaterialTheme.colorScheme.onSurface.toArgb(),
) )
}, },
badgesStart = coverBadgeStart, badgesStart = coverBadgeStart,
@ -313,14 +324,14 @@ private fun GridItemSelectable(
private fun Modifier.selectedOutline( private fun Modifier.selectedOutline(
isSelected: Boolean, isSelected: Boolean,
color: Color, color: Color,
) = this then drawBehind { if (isSelected) drawRect(color = color) } ) = this.drawBehind { if (isSelected) drawRect(color = color) }
/** /**
* Layout of list item. * Layout of list item.
*/ */
@Composable @Composable
fun MangaListItem( fun MangaListItem(
coverData: tachiyomi.domain.manga.model.MangaCover, coverData: DomainMangaCover,
title: String, title: String,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit, onLongClick: () -> Unit,
@ -343,8 +354,14 @@ fun MangaListItem(
MangaCover.Square( MangaCover.Square(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.alpha(coverAlpha), .background(
color = DomainMangaCover.coverColorMap[coverData.mangaId]?.first?.let { Color(it) }
?: MaterialTheme.colorScheme.surface
),
alpha = coverAlpha,
data = coverData, data = coverData,
tint = DomainMangaCover.coverColorMap[coverData.mangaId]?.second
?: MaterialTheme.colorScheme.onSurface.toArgb(),
) )
Text( Text(
text = title, text = title,

View file

@ -1,19 +1,28 @@
package eu.kanade.presentation.manga.components package eu.kanade.presentation.manga.components
import androidx.annotation.ColorInt
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import eu.kanade.presentation.util.rememberResourceBitmapPainter import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.coroutines.delay
enum class MangaCover(val ratio: Float) { enum class MangaCover(val ratio: Float) {
Square(1f / 1f), Square(1f / 1f),
@ -27,15 +36,36 @@ enum class MangaCover(val ratio: Float) {
contentDescription: String = "", contentDescription: String = "",
shape: Shape = MaterialTheme.shapes.extraSmall, shape: Shape = MaterialTheme.shapes.extraSmall,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
@ColorInt tint: Int = CoverPlaceholderColor,
alpha: Float = 1f,
) { ) {
val animatedImageVector = AnimatedImageVector.animatedVectorResource(R.drawable.anim_waiting)
var atEnd by remember { mutableStateOf(false) }
suspend fun runAnimation() {
while (true) {
delay(LoadingAnimatedDuration)
atEnd = !atEnd
}
}
LaunchedEffect(animatedImageVector) {
runAnimation()
}
var succeed by remember { mutableStateOf(false) }
AsyncImage( AsyncImage(
model = data, model = data,
placeholder = ColorPainter(CoverPlaceholderColor), placeholder = rememberAnimatedVectorPainter(animatedImageVector = animatedImageVector, atEnd = atEnd),
error = rememberResourceBitmapPainter(id = R.drawable.cover_error), error = rememberResourceBitmapPainter(id = R.drawable.cover_error, tint),
fallback = rememberResourceBitmapPainter(id = R.drawable.cover_error, tint),
onSuccess = { succeed = true },
contentDescription = contentDescription, contentDescription = contentDescription,
modifier = modifier modifier = modifier
.aspectRatio(ratio) .aspectRatio(ratio)
.clip(shape) .clip(shape)
.alpha(if (succeed) alpha else 1f)
.then( .then(
if (onClick != null) { if (onClick != null) {
Modifier.clickable( Modifier.clickable(
@ -51,4 +81,6 @@ enum class MangaCover(val ratio: Float) {
} }
} }
private val CoverPlaceholderColor = Color(0x1F888888) @ColorInt
internal val CoverPlaceholderColor = 0x7F888888
const val LoadingAnimatedDuration = 600L

View file

@ -1,6 +1,7 @@
package eu.kanade.presentation.util package eu.kanade.presentation.util
import android.content.res.Resources import android.content.res.Resources
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -19,11 +20,12 @@ import androidx.core.graphics.drawable.toBitmap
* @return the bitmap associated with the resource * @return the bitmap associated with the resource
*/ */
@Composable @Composable
fun rememberResourceBitmapPainter(@DrawableRes id: Int): BitmapPainter { fun rememberResourceBitmapPainter(@DrawableRes id: Int, @ColorInt tint: Int? = null): BitmapPainter {
val context = LocalContext.current val context = LocalContext.current
return remember(id) { return remember(id) {
val drawable = ContextCompat.getDrawable(context, id) val drawable = ContextCompat.getDrawable(context, id)
?: throw Resources.NotFoundException() ?: throw Resources.NotFoundException()
tint?.let { drawable.setTint(it) }
BitmapPainter(drawable.toBitmap().asImageBitmap()) BitmapPainter(drawable.toBitmap().asImageBitmap())
} }
} }

View file

@ -0,0 +1,209 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:width="24dp"
android:height="24dp"
android:viewportWidth="4320"
android:viewportHeight="4320">
<group android:name="_SQ">
<path
android:fillAlpha="0.1"
android:fillColor="@color/cover_placeholder_loading"
android:pathData="M0,0h4320v4320H0z" />
</group>
<group android:name="_R_G"
android:translateX="1620"
android:translateY="1620">
<group android:name="_R_G_L_2_G">
<path
android:name="_R_G_L_2_G_D_0_P_0"
android:fillAlpha="1"
android:fillColor="@color/cover_placeholder_loading"
android:fillType="nonZero"
android:pathData=" M270.46 450.51 C220.99,450.51 180.52,490.99 180.52,540.46 C180.52,589.93 220.99,630.41 270.46,630.41 C319.93,630.41 360.41,589.93 360.41,540.46 C360.41,490.99 319.93,450.51 270.46,450.51c " />
</group>
<group android:name="_R_G_L_1_G">
<path
android:name="_R_G_L_1_G_D_0_P_0"
android:fillAlpha="1"
android:fillColor="@color/cover_placeholder_loading"
android:fillType="nonZero"
android:pathData=" M540.31 450.51 C490.84,450.51 450.36,490.99 450.36,540.46 C450.36,589.93 490.84,630.41 540.31,630.41 C589.78,630.41 630.26,589.93 630.26,540.46 C630.26,490.99 589.78,450.51 540.31,450.51c " />
</group>
<group android:name="_R_G_L_0_G">
<path
android:name="_R_G_L_0_G_D_0_P_0"
android:fillAlpha="1"
android:fillColor="@color/cover_placeholder_loading"
android:fillType="nonZero"
android:pathData=" M810.16 450.51 C760.68,450.51 720.21,490.99 720.21,540.46 C720.21,589.93 760.68,630.41 810.16,630.41 C859.63,630.41 900.1,589.93 900.1,540.46 C900.1,490.99 859.63,450.51 810.16,450.51c " />
</group>
</group>
<group android:name="time_group" />
</vector>
</aapt:attr>
<target android:name="_R_G_L_2_G_D_0_P_0">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="83"
android:propertyName="pathData"
android:startOffset="0"
android:valueFrom="M270.46 450.51 C220.99,450.51 180.52,490.99 180.52,540.46 C180.52,589.93 220.99,630.41 270.46,630.41 C319.93,630.41 360.41,589.93 360.41,540.46 C360.41,490.99 319.93,450.51 270.46,450.51c "
android:valueTo="M270.46 450.51 C220.99,450.51 180.52,490.99 180.52,540.46 C180.52,589.93 220.99,630.41 270.46,630.41 C319.93,630.41 360.41,589.93 360.41,540.46 C360.41,490.99 319.93,450.51 270.46,450.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="100"
android:propertyName="pathData"
android:startOffset="83"
android:valueFrom="M270.46 450.51 C220.99,450.51 180.52,490.99 180.52,540.46 C180.52,589.93 220.99,630.41 270.46,630.41 C319.93,630.41 360.41,589.93 360.41,540.46 C360.41,490.99 319.93,450.51 270.46,450.51c "
android:valueTo="M272.31 322.51 C222.84,322.51 182.36,362.99 182.36,412.46 C182.36,461.93 222.84,502.41 272.31,502.41 C321.78,502.41 362.26,461.93 362.26,412.46 C362.26,362.99 321.78,322.51 272.31,322.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="133"
android:propertyName="pathData"
android:startOffset="183"
android:valueFrom="M272.31 322.51 C222.84,322.51 182.36,362.99 182.36,412.46 C182.36,461.93 222.84,502.41 272.31,502.41 C321.78,502.41 362.26,461.93 362.26,412.46 C362.26,362.99 321.78,322.51 272.31,322.51c "
android:valueTo="M270.46 486.51 C220.99,486.51 180.52,526.99 180.52,576.46 C180.52,625.93 220.99,666.41 270.46,666.41 C319.93,666.41 360.41,625.93 360.41,576.46 C360.41,526.99 319.93,486.51 270.46,486.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="67"
android:propertyName="pathData"
android:startOffset="317"
android:valueFrom="M270.46 486.51 C220.99,486.51 180.52,526.99 180.52,576.46 C180.52,625.93 220.99,666.41 270.46,666.41 C319.93,666.41 360.41,625.93 360.41,576.46 C360.41,526.99 319.93,486.51 270.46,486.51c "
android:valueTo="M270.46 450.51 C220.99,450.51 180.52,490.99 180.52,540.46 C180.52,589.93 220.99,630.41 270.46,630.41 C319.93,630.41 360.41,589.93 360.41,540.46 C360.41,490.99 319.93,450.51 270.46,450.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
<target android:name="_R_G_L_1_G_D_0_P_0">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="150"
android:propertyName="pathData"
android:startOffset="0"
android:valueFrom="M540.31 450.51 C490.84,450.51 450.36,490.99 450.36,540.46 C450.36,589.93 490.84,630.41 540.31,630.41 C589.78,630.41 630.26,589.93 630.26,540.46 C630.26,490.99 589.78,450.51 540.31,450.51c "
android:valueTo="M540.31 450.51 C490.84,450.51 450.36,490.99 450.36,540.46 C450.36,589.93 490.84,630.41 540.31,630.41 C589.78,630.41 630.26,589.93 630.26,540.46 C630.26,490.99 589.78,450.51 540.31,450.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="100"
android:propertyName="pathData"
android:startOffset="150"
android:valueFrom="M540.31 450.51 C490.84,450.51 450.36,490.99 450.36,540.46 C450.36,589.93 490.84,630.41 540.31,630.41 C589.78,630.41 630.26,589.93 630.26,540.46 C630.26,490.99 589.78,450.51 540.31,450.51c "
android:valueTo="M542.16 322.51 C492.68,322.51 452.21,362.99 452.21,412.46 C452.21,461.93 492.68,502.41 542.16,502.41 C591.63,502.41 632.1,461.93 632.1,412.46 C632.1,362.99 591.63,322.51 542.16,322.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="133"
android:propertyName="pathData"
android:startOffset="250"
android:valueFrom="M542.16 322.51 C492.68,322.51 452.21,362.99 452.21,412.46 C452.21,461.93 492.68,502.41 542.16,502.41 C591.63,502.41 632.1,461.93 632.1,412.46 C632.1,362.99 591.63,322.51 542.16,322.51c "
android:valueTo="M540.31 486.51 C490.84,486.51 450.36,526.99 450.36,576.46 C450.36,625.93 490.84,666.41 540.31,666.41 C589.78,666.41 630.26,625.93 630.26,576.46 C630.26,526.99 589.78,486.51 540.31,486.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="67"
android:propertyName="pathData"
android:startOffset="383"
android:valueFrom="M540.31 486.51 C490.84,486.51 450.36,526.99 450.36,576.46 C450.36,625.93 490.84,666.41 540.31,666.41 C589.78,666.41 630.26,625.93 630.26,576.46 C630.26,526.99 589.78,486.51 540.31,486.51c "
android:valueTo="M540.31 450.51 C490.84,450.51 450.36,490.99 450.36,540.46 C450.36,589.93 490.84,630.41 540.31,630.41 C589.78,630.41 630.26,589.93 630.26,540.46 C630.26,490.99 589.78,450.51 540.31,450.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
<target android:name="_R_G_L_0_G_D_0_P_0">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="217"
android:propertyName="pathData"
android:startOffset="0"
android:valueFrom="M810.16 450.51 C760.68,450.51 720.21,490.99 720.21,540.46 C720.21,589.93 760.68,630.41 810.16,630.41 C859.63,630.41 900.1,589.93 900.1,540.46 C900.1,490.99 859.63,450.51 810.16,450.51c "
android:valueTo="M810.16 450.51 C760.68,450.51 720.21,490.99 720.21,540.46 C720.21,589.93 760.68,630.41 810.16,630.41 C859.63,630.41 900.1,589.93 900.1,540.46 C900.1,490.99 859.63,450.51 810.16,450.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="100"
android:propertyName="pathData"
android:startOffset="217"
android:valueFrom="M810.16 450.51 C760.68,450.51 720.21,490.99 720.21,540.46 C720.21,589.93 760.68,630.41 810.16,630.41 C859.63,630.41 900.1,589.93 900.1,540.46 C900.1,490.99 859.63,450.51 810.16,450.51c "
android:valueTo="M812 322.51 C762.53,322.51 722.05,362.99 722.05,412.46 C722.05,461.93 762.53,502.41 812,502.41 C861.47,502.41 901.95,461.93 901.95,412.46 C901.95,362.99 861.47,322.51 812,322.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="133"
android:propertyName="pathData"
android:startOffset="317"
android:valueFrom="M812 322.51 C762.53,322.51 722.05,362.99 722.05,412.46 C722.05,461.93 762.53,502.41 812,502.41 C861.47,502.41 901.95,461.93 901.95,412.46 C901.95,362.99 861.47,322.51 812,322.51c "
android:valueTo="M810.16 486.51 C760.68,486.51 720.21,526.99 720.21,576.46 C720.21,625.93 760.68,666.41 810.16,666.41 C859.63,666.41 900.1,625.93 900.1,576.46 C900.1,526.99 859.63,486.51 810.16,486.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
<objectAnimator
android:duration="67"
android:propertyName="pathData"
android:startOffset="450"
android:valueFrom="M810.16 486.51 C760.68,486.51 720.21,526.99 720.21,576.46 C720.21,625.93 760.68,666.41 810.16,666.41 C859.63,666.41 900.1,625.93 900.1,576.46 C900.1,526.99 859.63,486.51 810.16,486.51c "
android:valueTo="M810.16 450.51 C760.68,450.51 720.21,490.99 720.21,540.46 C720.21,589.93 760.68,630.41 810.16,630.41 C859.63,630.41 900.1,589.93 900.1,540.46 C900.1,490.99 859.63,450.51 810.16,450.51c "
android:valueType="pathType">
<aapt:attr name="android:interpolator">
<pathInterpolator android:pathData="M 0.0,0.0 c0.5,0 0.5,1 1.0,1.0" />
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
<target android:name="time_group">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:duration="518"
android:propertyName="translateX"
android:startOffset="0"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--suppress AndroidElementNotAllowed -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" <layer-list xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<item> <item>

View file

@ -2,7 +2,8 @@
<resources> <resources>
<color name="splash">#EAEDE3</color> <color name="splash">#EAEDE3</color>
<color name="cover_placeholder">#1F888888</color> <color name="cover_placeholder">#2F888888</color>
<color name="cover_placeholder_loading">#888888</color>
<color name="error">#BA1B1B</color> <color name="error">#BA1B1B</color>
<color name="errorContainer">#FFDAD4</color> <color name="errorContainer">#FFDAD4</color>

View file

@ -2,10 +2,12 @@ package tachiyomi.presentation.widget.components
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier import androidx.glance.GlanceModifier
import androidx.glance.Image import androidx.glance.Image
import androidx.glance.ImageProvider import androidx.glance.ImageProvider
import androidx.glance.background
import androidx.glance.layout.Box import androidx.glance.layout.Box
import androidx.glance.layout.ContentScale import androidx.glance.layout.ContentScale
import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxSize
@ -20,10 +22,12 @@ val CoverHeight = 87.dp
fun UpdatesMangaCover( fun UpdatesMangaCover(
cover: Bitmap?, cover: Bitmap?,
modifier: GlanceModifier = GlanceModifier, modifier: GlanceModifier = GlanceModifier,
color: Color? = null,
) { ) {
Box( Box(
modifier = modifier modifier = modifier
.size(width = CoverWidth, height = CoverHeight) .size(width = CoverWidth, height = CoverHeight)
.background(color ?: Color.Unspecified)
.appWidgetInnerRadius(), .appWidgetInnerRadius(),
) { ) {
if (cover != null) { if (cover != null) {

View file

@ -3,6 +3,7 @@ package tachiyomi.presentation.widget.components
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier import androidx.glance.GlanceModifier
@ -23,6 +24,7 @@ import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider import androidx.glance.unit.ColorProvider
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
@ -86,6 +88,7 @@ fun UpdatesWidget(
UpdatesMangaCover( UpdatesMangaCover(
cover = cover, cover = cover,
modifier = GlanceModifier.clickable(actionStartActivity(intent)), modifier = GlanceModifier.clickable(actionStartActivity(intent)),
color = MangaCover.coverColorMap[mangaId]?.first?.let { Color(it) },
) )
} }
} }