mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
fade cover art over the letter placeholder and lift CJK initials
This commit is contained in:
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
@@ -49,8 +50,11 @@ import coil3.size.Dimension
|
|||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import com.subtracks.ui.theme.artworkColorsFromSeed
|
import com.subtracks.ui.theme.artworkColorsFromSeed
|
||||||
import com.subtracks.ui.theme.placeholderSeed
|
import com.subtracks.ui.theme.placeholderSeed
|
||||||
|
import java.lang.Character.UnicodeScript
|
||||||
|
|
||||||
private const val MAX_CACHED_RATIOS = 256
|
private const val MAX_CACHED_RATIOS = 256
|
||||||
|
private const val COVER_ART_FADE_MILLIS = 100
|
||||||
|
private const val IDEOGRAPHIC_INITIAL_LIFT = 0.05f
|
||||||
|
|
||||||
private object ArtworkRatioCache {
|
private object ArtworkRatioCache {
|
||||||
private val ratios = LruCache<String, Float>(MAX_CACHED_RATIOS)
|
private val ratios = LruCache<String, Float>(MAX_CACHED_RATIOS)
|
||||||
@@ -92,7 +96,7 @@ fun CoverArt(
|
|||||||
.background(letterScheme?.primaryContainer ?: MaterialTheme.colorScheme.surfaceVariant)
|
.background(letterScheme?.primaryContainer ?: MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
|
||||||
if (square) {
|
if (square) {
|
||||||
val model = remember(ref, thumbnailRef) { ref?.let { imageRequest(context, it, crossfade = thumbnailRef != null) } }
|
val model = remember(ref, thumbnailRef) { ref?.let { imageRequest(context, it, crossfade = true) } }
|
||||||
Box(modifier.then(frameModifier)) {
|
Box(modifier.then(frameModifier)) {
|
||||||
CoverArtContent(
|
CoverArtContent(
|
||||||
ref = ref,
|
ref = ref,
|
||||||
@@ -203,11 +207,20 @@ private fun BoxScope.CoverArtContent(
|
|||||||
) {
|
) {
|
||||||
val context = LocalPlatformContext.current
|
val context = LocalPlatformContext.current
|
||||||
if (showPlaceholder || (failed && !thumbnailLoaded)) {
|
if (showPlaceholder || (failed && !thumbnailLoaded)) {
|
||||||
|
val trimmed = name.trim()
|
||||||
|
val letter = if (trimmed.isEmpty()) "" else trimmed.substring(0, trimmed.offsetByCodePoints(0, 1)).uppercase()
|
||||||
|
val lift =
|
||||||
|
with(LocalDensity.current) {
|
||||||
|
(MaterialTheme.typography.headlineMedium.fontSize * IDEOGRAPHIC_INITIAL_LIFT).toDp()
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = name.trim().take(1).uppercase(),
|
text = letter,
|
||||||
style = MaterialTheme.typography.headlineMedium,
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
color = placeholderContentColor ?: MaterialTheme.colorScheme.onSurfaceVariant,
|
color = placeholderContentColor ?: MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.offset(y = if (letter.isIdeographicInitial()) -lift else 0.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (thumbnailRef != null && thumbnailRef != ref) {
|
if (thumbnailRef != null && thumbnailRef != ref) {
|
||||||
@@ -233,6 +246,14 @@ private fun BoxScope.CoverArtContent(
|
|||||||
|
|
||||||
private fun Size.ratioOrNull(): Float? = if (width > 0f && height > 0f && width.isFinite() && height.isFinite()) width / height else null
|
private fun Size.ratioOrNull(): Float? = if (width > 0f && height > 0f && width.isFinite() && height.isFinite()) width / height else null
|
||||||
|
|
||||||
|
internal fun String.isIdeographicInitial(): Boolean {
|
||||||
|
if (isEmpty()) return false
|
||||||
|
return when (UnicodeScript.of(codePointAt(0))) {
|
||||||
|
UnicodeScript.HAN, UnicodeScript.HIRAGANA, UnicodeScript.KATAKANA, UnicodeScript.HANGUL -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun imageRequest(
|
internal fun imageRequest(
|
||||||
context: Context,
|
context: Context,
|
||||||
ref: CoverArtRef,
|
ref: CoverArtRef,
|
||||||
@@ -245,7 +266,7 @@ internal fun imageRequest(
|
|||||||
.Builder(context)
|
.Builder(context)
|
||||||
.data(ref.url)
|
.data(ref.url)
|
||||||
.diskCacheKey(ref.cacheKey)
|
.diskCacheKey(ref.cacheKey)
|
||||||
.crossfade(crossfade)
|
.crossfade(if (crossfade) COVER_ART_FADE_MILLIS else 0)
|
||||||
if (width == null && height == null) {
|
if (width == null && height == null) {
|
||||||
builder.memoryCacheKey(ref.cacheKey)
|
builder.memoryCacheKey(ref.cacheKey)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.subtracks.ui.components
|
||||||
|
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class CoverArtInitialTest {
|
||||||
|
@Test
|
||||||
|
fun ideographicInitialsAreDetected() {
|
||||||
|
assertTrue("東京".isIdeographicInitial())
|
||||||
|
assertTrue("あ".isIdeographicInitial())
|
||||||
|
assertTrue("カ".isIdeographicInitial())
|
||||||
|
assertTrue("한".isIdeographicInitial())
|
||||||
|
assertTrue("𠮟".isIdeographicInitial())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun latinInitialsAndEmptyNamesAreNotIdeographic() {
|
||||||
|
assertFalse("Abbey Road".isIdeographicInitial())
|
||||||
|
assertFalse("".isIdeographicInitial())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user