From a046790ab057d93ee5f519cced28ac69fae13895 Mon Sep 17 00:00:00 2001 From: zhushenwudi <55681140@163.com> Date: Sat, 26 Sep 2026 16:42:24 +0800 Subject: [PATCH] fix photo cache and setting config --- .../native/WebViewActivity.kt | 3 +- .../zhushenwudi/lovelivemusicplayer/App.kt | 6 +- .../lovelivemusicplayer/AppContainer.kt | 29 ++- .../lovelivemusicplayer/ui/MainShell.kt | 10 +- .../ui/components/LlmpDialogs.kt | 14 ++ .../ui/components/LlmpFlexibleScrollbar.kt | 3 +- .../lovelivemusicplayer/ui/icons/CoilSvg.kt | 2 +- .../ui/image/LlmpImageCache.kt | 168 +++++++++--------- .../ui/player/FullPlayerScreen.kt | 3 +- .../ui/screens/DrawerFeatureScreens.kt | 82 +++++---- .../ui/screens/SplashScreen.kt | 5 +- gradle/libs.versions.toml | 2 +- iosApp/iosApp/Info.plist | 2 +- .../player/PlayerController.android.kt | 3 +- .../lovelivemusicplayer/i18n/DeStrings.kt | 4 +- .../lovelivemusicplayer/i18n/EnStrings.kt | 4 +- .../lovelivemusicplayer/i18n/JaStrings.kt | 4 +- .../lovelivemusicplayer/i18n/ZhStrings.kt | 4 +- .../lovelivemusicplayer/player/SleepTimer.kt | 3 +- .../player/PlayerController.ios.kt | 37 +++- 20 files changed, 243 insertions(+), 145 deletions(-) diff --git a/androidApp/src/main/kotlin/com/zhushenwudi/lovelivemusicplayer/native/WebViewActivity.kt b/androidApp/src/main/kotlin/com/zhushenwudi/lovelivemusicplayer/native/WebViewActivity.kt index c6ba29a..ab796c2 100644 --- a/androidApp/src/main/kotlin/com/zhushenwudi/lovelivemusicplayer/native/WebViewActivity.kt +++ b/androidApp/src/main/kotlin/com/zhushenwudi/lovelivemusicplayer/native/WebViewActivity.kt @@ -45,6 +45,7 @@ import com.zhushenwudi.lovelivemusicplayer.i18n.tr import com.zhushenwudi.lovelivemusicplayer.platform.WebViewAdBlock import java.io.ByteArrayInputStream import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.seconds class WebViewActivity : ComponentActivity() { private var webView: WebView? = null @@ -81,7 +82,7 @@ class WebViewActivity : ComponentActivity() { } LaunchedEffect(Unit) { while (true) { - delay(1_000) + delay(1.seconds) webView?.evaluateJavascript(WebViewAdBlock.domCleanupScript(), null) } } diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/App.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/App.kt index fe79864..b87ce41 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/App.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/App.kt @@ -51,6 +51,7 @@ import com.zhushenwudi.lovelivemusicplayer.ui.screens.TachieScreen import com.zhushenwudi.lovelivemusicplayer.ui.screens.TransferScreen import com.zhushenwudi.lovelivemusicplayer.i18n.SyncI18nWithSystemLocale import com.zhushenwudi.lovelivemusicplayer.ui.theme.ProvideDesignDensity +import kotlin.time.Duration.Companion.milliseconds @Composable fun App() { @@ -85,7 +86,7 @@ fun App() { if (overlayOpen) { shellInFront = false } else { - delay(LlmpTransitionMs.toLong()) + delay(LlmpTransitionMs.toLong().milliseconds) shellInFront = true } } @@ -182,6 +183,9 @@ fun App() { settings.followSystemTheme = false }, onBack = { navController.popBackStack() }, + onReturnHome = { + navController.popBackStack(Routes.MAIN, inclusive = false) + }, onOpenSd = { navController.navigate(Routes.SD) }, onOpenLogs = { navController.navigate(Routes.LOGS) }, onOpenPermissions = { navController.navigate(Routes.PERMISSIONS) }, diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/AppContainer.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/AppContainer.kt index d988409..f53fdc3 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/AppContainer.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/AppContainer.kt @@ -48,6 +48,7 @@ import com.zhushenwudi.lovelivemusicplayer.ui.weslide.WeSlideController import com.zhushenwudi.lovelivemusicplayer.update.UpdateChecker import com.zhushenwudi.lovelivemusicplayer.util.PathResolver import com.zhushenwudi.lovelivemusicplayer.widget.WidgetCoverCache +import kotlin.time.Duration.Companion.seconds object AppContainer { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -64,6 +65,9 @@ object AppContainer { private val _usbRevision = MutableStateFlow(0L) private val _libraryRevision = MutableStateFlow(0L) + /** 清空用户数据后让首页收起详情和播放页。 */ + private val _shellReset = MutableStateFlow(0L) + /** 当前曲目完整歌词,切歌时刷新。 */ val playingLyric: StateFlow = _playingLyric.asStateFlow() @@ -78,6 +82,7 @@ object AppContainer { /** 切存储清库后递增,刷新曲库 UI。 */ val libraryRevision: StateFlow = _libraryRevision.asStateFlow() + val shellReset: StateFlow = _shellReset.asStateFlow() /** 主页 WeSlide 面板控制器。 */ val weSlide = WeSlideController() @@ -146,9 +151,10 @@ object AppContainer { this.pathResolver = PathResolver(fileSystem, settings) // musicPath 保持相对路径;仅播放引擎解析为 file/http URI。 player.resolvePlaybackUri = { pathResolver.playbackUri(it) } - // Android 通知封面用 file:// / https;iOS Now Playing 优先绝对路径。 + // 本地文件存在才用绝对路径。HTTP 曲库没有落盘封面时必须走 https, + // 否则负一屏 / 灵动岛会拿到一个不存在的路径,封面空白。 player.resolveCoverUri = { music -> - pathResolver.absoluteCoverPath(music) + pathResolver.absoluteCoverPath(music)?.takeIf(fileSystem::exists) ?: pathResolver.coverDisplayUri(music) } @@ -209,7 +215,7 @@ object AppContainer { bindUsbMountListener() // 全表快照放后台,并避开主页出来后的首屏解码。 scope.launch { - delay(2_000) + delay(2.seconds) refreshCarPlayCatalog() } } @@ -242,7 +248,7 @@ object AppContainer { private fun onUsbStorageChanged() { scope.launch { - delay(1_000) + delay(1.seconds) val pathList = fileSystem.listUsbRoots() if (pathList.isEmpty()) return@launch val defPath = settings.sdPath @@ -274,6 +280,21 @@ object AppContainer { _libraryRevision.value++ } + /** 清空喜欢、历史、歌单和播放队列,并通知首页刷新。 */ + fun clearUserData() { + if (!isInitialized) return + library.clearMenus() + library.clearLove() + library.clearHistory() + settings.dataVersion = "" + clearPlayQueue() + refreshCarPlayCatalog() + _libraryRevision.value++ + _loveRevision.value++ + _historyRevision.value++ + _shellReset.value++ + } + fun storageDisplayName(path: String): String { val key = path.split("/").getOrNull(2) ?: path.substringAfterLast('/').ifBlank { path } return if (key == "emulated") I18n.t("internal_storage") else key diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/MainShell.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/MainShell.kt index cc41ae5..e733e24 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/MainShell.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/MainShell.kt @@ -104,6 +104,8 @@ import com.zhushenwudi.lovelivemusicplayer.ui.theme.asUnscaledInScaledContext import com.zhushenwudi.lovelivemusicplayer.ui.theme.unscaledNavigationBarsBottom import com.zhushenwudi.lovelivemusicplayer.ui.theme.unscaledStatusBarsTop import com.zhushenwudi.lovelivemusicplayer.ui.weslide.WeSlide +import kotlinx.coroutines.IO +import kotlin.time.Duration.Companion.milliseconds @OptIn(ExperimentalSharedTransitionApi::class) @Composable @@ -250,6 +252,12 @@ fun MainShell( val historyRevision by AppContainer.historyRevision.collectAsState() val loveRevision by AppContainer.loveRevision.collectAsState() val libraryRevision by AppContainer.libraryRevision.collectAsState() + val shellReset by AppContainer.shellReset.collectAsState() + LaunchedEffect(shellReset) { + if (shellReset == 0L) return@LaunchedEffect + libraryDetail = null + weSlide.hide() + } val startupHistoryRevision = remember { historyRevision } val startupLoveRevision = remember { loveRevision } val libraryLists = remember(refreshToken, currentGroup, enableHttp, libraryRevision) { @@ -285,7 +293,7 @@ fun MainShell( } // 首屏封面先解码。本地文件核对延后,避免扫盘占满磁盘把列表拖卡。 LaunchedEffect(Unit) { - delay(1_500) + delay(1_500.milliseconds) homeHazeEnabled = true val groupAtStart = currentGroup.displayName val httpAtStart = enableHttp diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/LlmpDialogs.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/LlmpDialogs.kt index 11d7c75..7db03d0 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/LlmpDialogs.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/LlmpDialogs.kt @@ -223,6 +223,7 @@ fun AddMinDialog( @Composable fun ResetDataDialog( onClearSongs: () -> Unit, + onClearImageCache: () -> Unit, onClearUserData: () -> Unit, onDismiss: () -> Unit, ) { @@ -241,6 +242,11 @@ fun ResetDataDialog( onClearSongs() } Spacer(Modifier.height(24.dp)) + ResetActionButton(tr("clear_image_cache")) { + onDismiss() + onClearImageCache() + } + Spacer(Modifier.height(24.dp)) ResetActionButton(tr("remove_user_data_kmp")) { onDismiss() onClearUserData() @@ -254,6 +260,14 @@ fun ResetDataDialog( modifier = Modifier.padding(horizontal = 12.dp), ) Spacer(Modifier.height(3.dp)) + Text( + tr("explain_image_cache"), + color = if (dark) ColorMs.White else ColorMs.Text333, + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 12.dp), + ) + Spacer(Modifier.height(3.dp)) Text( tr("explain_user_data_kmp"), color = if (dark) ColorMs.White else ColorMs.Text333, diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/LlmpFlexibleScrollbar.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/LlmpFlexibleScrollbar.kt index e4282ae..0bbc1e2 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/LlmpFlexibleScrollbar.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/LlmpFlexibleScrollbar.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds private const val ThumbResizeDurationMs = 1_000 private const val ThumbFadeDelayMs = 650L @@ -207,7 +208,7 @@ private fun LlmpFlexibleScrollbarOverlay( if (snapshot.canScroll && (isScrollInProgress || isDragging)) { showThumb = true } else { - delay(ThumbFadeDelayMs) + delay(ThumbFadeDelayMs.milliseconds) showThumb = false } } diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/icons/CoilSvg.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/icons/CoilSvg.kt index 7b9bc07..487e29c 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/icons/CoilSvg.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/icons/CoilSvg.kt @@ -6,7 +6,7 @@ import coil3.compose.setSingletonImageLoaderFactory import com.zhushenwudi.lovelivemusicplayer.ui.image.createLlmpImageLoader /** - * 注册 Coil 单例:SVG 解码 + 1 天网络图片缓存(SWR)。 + * 注册 Coil 单例:SVG 解码 + 长期网络图片缓存。 * 网络 https(artist_webp 等)需要 classpath 上的 `coil-network-ktor3` — * Coil 3 无此模块不会拉取网络图片。 * 须在应用根节点、任何 AsyncImage 之前调用一次。 diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/image/LlmpImageCache.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/image/LlmpImageCache.kt index c0cc45c..8685163 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/image/LlmpImageCache.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/image/LlmpImageCache.kt @@ -1,7 +1,10 @@ package com.zhushenwudi.lovelivemusicplayer.ui.image import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -35,23 +38,18 @@ import coil3.request.Options import coil3.request.crossfade import coil3.size.Size import coil3.svg.SvgDecoder -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import com.zhushenwudi.lovelivemusicplayer.db.currentTimeMs import com.zhushenwudi.lovelivemusicplayer.platform.AppLog private const val IMG_TAG = "LLMP-Img" -/** 网络写入成功后的磁盘缓存软 TTL。 */ -private const val IMAGE_CACHE_TTL_MS = 24L * 60 * 60 * 1000 - /** * Coil 网络缓存策略: - * - `IMAGE_CACHE_TTL_MS` 内:仅使用磁盘缓存。 - * - 过期后:仍立即显示旧图(不空白/不闪烁),后台刷新;新字节写入缓存后再切换 UI。 + * - 成功图片一直留在磁盘,直到用户在设置里清除。 + * - 空正文、HTML/JSON/XML 不写入,读到这类缓存时改为走网络。 + * - 磁盘字节无法解码时丢掉该条目并立刻再请求。 */ @OptIn(ExperimentalCoilApi::class) -internal object DayTtlSwrCacheStrategy : CacheStrategy { +internal object PermanentImageCacheStrategy : CacheStrategy { override suspend fun read( cacheResponse: NetworkResponse, @@ -59,13 +57,15 @@ internal object DayTtlSwrCacheStrategy : CacheStrategy { options: Options, ): ReadResult { val url = networkRequest.url - if (cacheResponse.code !in 200 until 300) { + if (cacheResponse.code !in 200 until 300 || isUnusableImageResponse(cacheResponse)) { + AppLog.w( + IMG_TAG, + "diskRead SKIP unusable code=${cacheResponse.code} " + + "type=${cacheResponse.headers["content-type"]} " + + "len=${cacheResponse.headers["content-length"]} url=$url", + ) return ReadResult(networkRequest) } - if (isExpired(cacheResponse)) { - ImageRevalidator.schedule(url, options.context) - } - // 显示时优先磁盘图,避免过期导致 UI 空白。 return ReadResult(cacheResponse) } @@ -85,90 +85,51 @@ internal object DayTtlSwrCacheStrategy : CacheStrategy { ), ) } - // 仅持久化成功图片;失败请求下次访问重试。 - if (networkResponse.code in 200 until 300) { + // 仅持久化可解码图片。空正文和 HTML 等不算成功,下次访问重试。 + if (networkResponse.code in 200 until 300 && !isUnusableImageResponse(networkResponse)) { return WriteResult(networkResponse) } AppLog.w( IMG_TAG, - "diskWrite SKIP code=${networkResponse.code} url=$url", + "diskWrite SKIP code=${networkResponse.code} " + + "type=${networkResponse.headers["content-type"]} " + + "len=${networkResponse.headers["content-length"]} url=$url", ) return WriteResult.DISABLED } - - private fun isExpired(cacheResponse: NetworkResponse): Boolean { - val cachedAt = maxOf(cacheResponse.requestMillis, cacheResponse.responseMillis) - // 无时间戳视为新鲜;标为过期会触发后台 - // 重新校验 + UI 重挂载,可能 Error 并闪烁/卡住占位图。 - if (cachedAt <= 0L) return false - return currentTimeMs() - cachedAt >= IMAGE_CACHE_TTL_MS - } } internal object ImageRevalidator { - private val mutex = Mutex() - private val inFlight = mutableSetOf() - private var imageLoader: ImageLoader? = null fun bind(loader: ImageLoader) { imageLoader = loader } - suspend fun schedule(url: String, context: PlatformContext) { - if (!url.isHttpUrl()) return - val cacheKey = url.toImageCacheKey() - mutex.withLock { - if (!inFlight.add(cacheKey)) { - return - } + /** 丢掉无法解码的磁盘/内存条目,下次访问重新请求。 */ + fun evictUndecodable(diskKey: String, memoryKey: String?) { + val diskRemoved = imageLoader?.diskCache?.remove(diskKey) == true + val memory = imageLoader?.memoryCache + memory?.remove(MemoryCache.Key(diskKey)) + if (memoryKey != null && memoryKey != diskKey) { + memory?.remove(MemoryCache.Key(memoryKey)) } - val loader = imageLoader - if (loader == null) { - mutex.withLock { inFlight.remove(cacheKey) } - AppLog.w(IMG_TAG, "revalidate skip-noLoader url=$url") - return - } - loader.enqueue( - ImageRequest.Builder(context) - .data(url) - .memoryCacheKey(cacheKey) - .diskCacheKey(cacheKey) - // 跳过读取过期磁盘项;强制网络拉取后重写缓存。 - .diskCachePolicy(CachePolicy.WRITE_ONLY) - .memoryCachePolicy(CachePolicy.ENABLED) - .networkCachePolicy(CachePolicy.ENABLED) - .crossfade(false) - .listener( - onSuccess = { _, _ -> - // 只刷新 Coil 磁盘/内存;不写入 CoverImageHolder, - // 避免无尺寸的原图覆盖列表缩略图。 - complete(cacheKey) - }, - onError = { _, result -> - AppLog.w( - IMG_TAG, - "revalidate ERR ${result.throwable.message} url=$url", - ) - complete(cacheKey) - }, - onCancel = { _ -> - complete(cacheKey) - }, - ) - .build(), - ) + AppLog.w(IMG_TAG, "evict undecodable diskRemoved=$diskRemoved key=$diskKey") } - private fun complete(cacheKey: String) { - // 监听器在 CacheStrategy 协程外运行;尽力而为,不挂起。 - inFlight.remove(cacheKey) - // 故意不触发 UI 版本更新:重新校验后重挂载可见封面 - // 曾导致间歇 Error → 占位图锁定。磁盘/内存已更新,供 - // 下次组合使用(如滚回)。 + fun clearAll() { + imageLoader?.memoryCache?.clear() + imageLoader?.diskCache?.clear() + CoverImageHolder.clear() + AppLog.i(IMG_TAG, "image cache cleared") } } +/** 清掉内存和磁盘里的网络图片。已显示的封面在下次进入页面时重新加载。 */ +fun clearLlmpImageCache() { + ImageRevalidator.clearAll() +} + /** * 封面解码 [androidx.compose.ui.graphics.Image] 的强引用。 @@ -192,6 +153,11 @@ internal object CoverImageHolder { map.remove(old) } } + + fun clear() { + keys.clear() + map.clear() + } } /** 规范化缓存键,使原始 `'`(如 μ's)与 `%27` 编码一致。 */ @@ -226,7 +192,7 @@ fun createLlmpImageLoader(context: PlatformContext): ImageLoader { add(SvgDecoder.Factory()) add( KtorNetworkFetcherFactory( - cacheStrategy = { DayTtlSwrCacheStrategy }, + cacheStrategy = { PermanentImageCacheStrategy }, ), ) } @@ -252,7 +218,7 @@ internal fun decodeSizeBucketPx(px: Int): Int = when { internal fun sizedMemoryCacheKey(baseKey: String, bucketPx: Int): String = if (bucketPx > 0) "$baseKey@$bucketPx" else baseKey -/** 共享 AsyncImage:1 天网络缓存 + 过期先用旧图(无闪烁)。 */ +/** 共享 AsyncImage:成功图片长期缓存,坏图丢弃后重拉一次。 */ @Composable fun LlmpAsyncImage( model: Any?, @@ -287,9 +253,9 @@ fun LlmpAsyncImage( else -> filterQuality } - // 后台重新校验时不重挂载。TTL 刷新后重挂载可见封面 - // 可能 Error 一次并使占位图锁定,行仍在屏幕上。 - val request = remember(model, httpUrl, baseCacheKey, memoryKey, bucketPx) { + // 解码失败只重拉一次。同一条目内不再循环,避免坏响应把占位图卡死。 + var refetchWithoutDisk by remember(model, baseCacheKey) { mutableStateOf(false) } + val request = remember(model, httpUrl, baseCacheKey, memoryKey, bucketPx, refetchWithoutDisk) { ImageRequest.Builder(context) .data(model) .crossfade(false) @@ -297,6 +263,9 @@ fun LlmpAsyncImage( if (bucketPx > 0) { size(Size(bucketPx, bucketPx)) } + if (refetchWithoutDisk) { + diskCachePolicy(CachePolicy.WRITE_ONLY) + } if (baseCacheKey != null) { // 磁盘存原图字节(无尺寸后缀);内存按显示尺寸分桶。 diskCacheKey(baseCacheKey) @@ -315,6 +284,10 @@ fun LlmpAsyncImage( IMG_TAG, "req ERR ${result.throwable.message} url=$model", ) + if (!refetchWithoutDisk && result.throwable.isUndecodableImage()) { + ImageRevalidator.evictUndecodable(baseCacheKey, memoryKey) + refetchWithoutDisk = true + } }, ) } @@ -379,3 +352,36 @@ fun LlmpAsyncImage( private fun String.isHttpUrl(): Boolean = startsWith("http://", ignoreCase = true) || startsWith("https://", ignoreCase = true) + +/** 空正文或明确的非图片类型。这种 2xx 不能占住磁盘 TTL。 */ +private fun isUnusableImageResponse(response: NetworkResponse): Boolean { + val length = response.headers["content-length"]?.trim()?.toLongOrNull() + if (length != null && length <= 0L) return true + val type = response.headers["content-type"] + ?.substringBefore(';') + ?.trim() + ?.lowercase() + ?: return false + return type == "text/html" || + type == "application/xhtml+xml" || + type == "application/json" || + type == "text/json" || + type == "application/xml" || + type == "text/xml" +} + +private fun Throwable.isUndecodableImage(): Boolean { + var current: Throwable? = this + while (current != null) { + val message = current.message.orEmpty() + if ( + "makeFromEncoded" in message || + "null bitmap" in message || + "Unable to create a decoder" in message + ) { + return true + } + current = current.cause + } + return false +} diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/player/FullPlayerScreen.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/player/FullPlayerScreen.kt index 565a0d7..03aaded 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/player/FullPlayerScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/player/FullPlayerScreen.kt @@ -95,6 +95,7 @@ import com.zhushenwudi.lovelivemusicplayer.ui.components.rememberMusicCoverUri import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons import kotlin.math.abs import kotlin.math.roundToLong +import kotlin.time.Duration.Companion.seconds /** 播放器内容类型:封面 ↔ 歌词(立绘暂未实现)。 */ private enum class PlayerContent { @@ -652,7 +653,7 @@ private fun PlayerLyricScroll( LaunchedEffect(isDragged, listState.isScrollInProgress, selecting) { if (!selecting) return@LaunchedEffect if (isDragged || listState.isScrollInProgress) return@LaunchedEffect - delay(3_000) + delay(3.seconds) if (!isDragged && !listState.isScrollInProgress) { selecting = false selectedIndex = -1 diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/screens/DrawerFeatureScreens.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/screens/DrawerFeatureScreens.kt index 21ae662..644ea4b 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/screens/DrawerFeatureScreens.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/screens/DrawerFeatureScreens.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -55,19 +54,23 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle +import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -83,6 +86,7 @@ import com.zhushenwudi.lovelivemusicplayer.platform.DetailBackHandler import com.zhushenwudi.lovelivemusicplayer.platform.PlatformUi import com.zhushenwudi.lovelivemusicplayer.player.LoopMode import com.zhushenwudi.lovelivemusicplayer.ui.image.LlmpAsyncImage +import com.zhushenwudi.lovelivemusicplayer.ui.image.clearLlmpImageCache import com.zhushenwudi.lovelivemusicplayer.theme.ColorMs import com.zhushenwudi.lovelivemusicplayer.theme.LocalDarkTheme import com.zhushenwudi.lovelivemusicplayer.ui.components.AddMinDialog @@ -106,7 +110,9 @@ import com.zhushenwudi.lovelivemusicplayer.ui.icons.SvgAsset import com.zhushenwudi.lovelivemusicplayer.ui.icons.SvgIcon import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalDesignScale import com.zhushenwudi.lovelivemusicplayer.ui.theme.hSp +import kotlinx.coroutines.IO import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds private val dailyJson = Json { ignoreUnknownKeys = true; isLenient = true } @@ -147,6 +153,7 @@ fun SettingsScreen( onFollowSystemChange: (Boolean) -> Unit, onDarkThemeChange: (Boolean) -> Unit, onBack: () -> Unit, + onReturnHome: () -> Unit = {}, onOpenSd: () -> Unit = {}, onOpenLogs: () -> Unit = {}, onOpenPermissions: () -> Unit = {}, @@ -333,17 +340,21 @@ fun SettingsScreen( PlatformUi.showToast(I18n.t("clean_success")) } }, + onClearImageCache = { + scope.launch(Dispatchers.IO) { + clearLlmpImageCache() + withContext(Dispatchers.Main) { + PlatformUi.showToast(I18n.t("clean_success")) + } + } + }, onClearUserData = { scope.launch { - loadingMsg = I18n.t("exiting") - AppContainer.library.clearMenus() - AppContainer.library.clearLove() - AppContainer.library.clearHistory() - AppContainer.library.clearPlayList() - settings.dataVersion = "" + loadingMsg = I18n.t("resetting") + AppContainer.clearUserData() loadingMsg = null PlatformUi.showToast(I18n.t("user_data_cleared")) - PlatformUi.exitApp() + onReturnHome() } }, onDismiss = { showClearDialog = false }, @@ -462,7 +473,7 @@ fun TransferScreen(onBack: () -> Unit, onOpenScan: () -> Unit) { LaunchedEffect(state.completed) { if (state.completed && !wasCompleted) { // 等本帧最后一次封面更新进入动画队列,再等轮播完整播放完毕。 - delay(100) + delay(100.milliseconds) snapshotFlow { coverAnimationsIdle }.first { it } onBack() } @@ -1258,14 +1269,20 @@ private fun DailySection( } } -private const val PrivacyLinkTag = "privacy_link" - private const val PrivacyUmengUrl = "https://www.umeng.com/page/policy" private const val PrivacyShareUrl = "https://policy.zztfly.com/sdk/share/privacy" private const val Privacy360Url = "https://jiagu.360.cn/#/global/help/322" private const val PrivacyGithubUrl = "https://github.com/zhushenwudi/LoveLiveMusicPlayerMobile" +private fun privacyLink( + url: String, + styles: TextLinkStyles, + title: String? = null, +): LinkAnnotation = LinkAnnotation.Clickable(tag = url, styles = styles) { + PlatformUi.openWebView(url, title ?: I18n.t("privacy_policy")) +} + @Composable fun PermissionsScreen(onBack: () -> Unit) { val dark = LocalDarkTheme.current @@ -1274,30 +1291,24 @@ fun PermissionsScreen(onBack: () -> Unit) { val linkColor = ColorMs.Blue28B3F7 val bodySize = hSp(12f) val bodyStyle = TextStyle(color = bodyColor, fontSize = bodySize, lineHeight = bodySize * 1.4f) - val linkStyle = SpanStyle(color = linkColor, fontSize = bodySize) + val linkStyles = TextLinkStyles(style = SpanStyle(color = linkColor, fontSize = bodySize)) val lang by I18n.lang.collectAsState() - val annotated = remember(bodyColor, bodySize, lang) { + val annotated = remember(bodySize, lang) { buildAnnotatedString { append(I18n.t("privacy_detail1")) - pushStringAnnotation(PrivacyLinkTag, PrivacyUmengUrl) - withStyle(linkStyle) { append(I18n.t("privacy_umeng")) } - pop() + withLink(privacyLink(PrivacyUmengUrl, linkStyles)) { append(I18n.t("privacy_umeng")) } append(I18n.t("privacy_detail2")) - pushStringAnnotation(PrivacyLinkTag, PrivacyShareUrl) - withStyle(linkStyle) { append(I18n.t("privacy_share")) } - pop() + withLink(privacyLink(PrivacyShareUrl, linkStyles)) { append(I18n.t("privacy_share")) } append(I18n.t("privacy_detail3")) - pushStringAnnotation(PrivacyLinkTag, Privacy360Url) - withStyle(linkStyle) { append(I18n.t("privacy_360")) } - pop() + withLink(privacyLink(Privacy360Url, linkStyles)) { append(I18n.t("privacy_360")) } append(I18n.t("privacy_detail4")) } } val githubLink = remember(bodySize, lang) { buildAnnotatedString { - pushStringAnnotation(PrivacyLinkTag, PrivacyGithubUrl) - withStyle(linkStyle) { append(I18n.t("github_url")) } - pop() + withLink(privacyLink(PrivacyGithubUrl, linkStyles, title = "GitHub")) { + append(I18n.t("github_url")) + } } } Scaffold( @@ -1316,25 +1327,12 @@ fun PermissionsScreen(onBack: () -> Unit) { .padding(horizontal = 12.dp) .navigationBarsPadding(), ) { - ClickableText( - text = annotated, - style = bodyStyle, - onClick = { offset -> - annotated.getStringAnnotations(PrivacyLinkTag, offset, offset) - .firstOrNull() - ?.let { PlatformUi.openWebView(it.item, I18n.t("privacy_policy")) } - }, - ) + Text(text = annotated, style = bodyStyle) Spacer(Modifier.height(12.dp)) - ClickableText( + Text( text = githubLink, style = bodyStyle.copy(textAlign = TextAlign.Center), modifier = Modifier.fillMaxWidth(), - onClick = { offset -> - githubLink.getStringAnnotations(PrivacyLinkTag, offset, offset) - .firstOrNull() - ?.let { PlatformUi.openWebView(it.item, "GitHub") } - }, ) Spacer(Modifier.height(12.dp)) } @@ -1350,7 +1348,7 @@ fun SdScreen(onBack: () -> Unit) { devices = AppContainer.listStorageDevices() return@LaunchedEffect } - delay(1_000) + delay(1.seconds) devices = AppContainer.listStorageDevices() } val dark = LocalDarkTheme.current diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/screens/SplashScreen.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/screens/SplashScreen.kt index a3cd6cf..a1c7c4c 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/screens/SplashScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/screens/SplashScreen.kt @@ -29,6 +29,7 @@ import com.zhushenwudi.lovelivemusicplayer.generated.resources.Res import com.zhushenwudi.lovelivemusicplayer.i18n.tr import com.zhushenwudi.lovelivemusicplayer.theme.ColorMs import com.zhushenwudi.lovelivemusicplayer.ui.image.LlmpAsyncImage +import kotlin.time.Duration.Companion.seconds /** * 启动页流程: @@ -53,7 +54,7 @@ fun SplashScreen(onFinished: () -> Unit) { LaunchedEffect(Unit) { // 先显示 1 秒 Logo - delay(1_000) + delay(1.seconds) count = 2 photoPath = runCatching { AppContainer.splashPhoto.pickCachedDisplayPath() @@ -61,7 +62,7 @@ fun SplashScreen(onFinished: () -> Unit) { // 剩余倒计时(2 秒 → 0)后进入主页 while (count > 0) { - delay(1_000) + delay(1.seconds) count -= 1 } goHome() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ab44851..4566387 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.3.1" +agp = "9.4.1" android-compileSdk = "37" android-minSdk = "24" android-targetSdk = "37" diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index c329fb5..483e8ae 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -173,6 +173,6 @@ UIInterfaceOrientationPortrait UIViewControllerBasedStatusBarAppearance - + diff --git a/shared/src/androidMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/PlayerController.android.kt b/shared/src/androidMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/PlayerController.android.kt index e2d2bc3..7e23771 100644 --- a/shared/src/androidMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/PlayerController.android.kt +++ b/shared/src/androidMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/PlayerController.android.kt @@ -23,6 +23,7 @@ import com.zhushenwudi.lovelivemusicplayer.platform.AppLog import com.zhushenwudi.lovelivemusicplayer.platform.PlatformServices import java.io.File import kotlin.random.Random +import kotlin.time.Duration.Companion.milliseconds actual class PlayerController(context: Context) { private val appContext = context.applicationContext @@ -114,7 +115,7 @@ actual class PlayerController(context: Context) { durationMs = player.duration.coerceAtLeast(0), ) } - delay(200) + delay(200.milliseconds) } } } diff --git a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/DeStrings.kt b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/DeStrings.kt index c6487df..61882f2 100644 --- a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/DeStrings.kt +++ b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/DeStrings.kt @@ -284,8 +284,10 @@ internal val DeStrings: Map = mapOf( "now_playing_song" to "Läuft gerade: %s", "fetch_songs_fail_or_net" to "Datenfehler oder Netzwerkfehler", "explain_songs_data_kmp" to "Songdaten: Bibliotheksmetadaten, Version", - "explain_user_data_kmp" to "Benutzerdaten: Likes/Verlauf/Playlists/Cache, dann beenden", + "explain_image_cache" to "Bildcache: heruntergeladene Cover, danach neu laden", + "explain_user_data_kmp" to "Benutzerdaten: Likes/Verlauf/Playlists/Cache", "remove_songs_data_kmp" to "Songdaten löschen", + "clear_image_cache" to "Bildcache löschen", "remove_user_data_kmp" to "Benutzerdaten löschen", "confirm_delete_from_menu_short" to "Aus Playlist löschen?", "play_text" to "Spielen,Pausierte", diff --git a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/EnStrings.kt b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/EnStrings.kt index 40aee29..44ac56c 100644 --- a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/EnStrings.kt +++ b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/EnStrings.kt @@ -284,8 +284,10 @@ internal val EnStrings: Map = mapOf( "now_playing_song" to "Now Playing: %s", "fetch_songs_fail_or_net" to "Data error or network failure", "explain_songs_data_kmp" to "Song data: library metadata, version", - "explain_user_data_kmp" to "User data: likes/history/playlists/cache, then quit", + "explain_image_cache" to "Image cache: downloaded covers, reloaded after clearing", + "explain_user_data_kmp" to "User data: likes/history/playlists/cache", "remove_songs_data_kmp" to "Clear song data", + "clear_image_cache" to "Clear image cache", "remove_user_data_kmp" to "Clear user data", "confirm_delete_from_menu_short" to "Delete from playlist?", "play_text" to "Playing,Paused", diff --git a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/JaStrings.kt b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/JaStrings.kt index 2d74844..1205d14 100644 --- a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/JaStrings.kt +++ b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/JaStrings.kt @@ -284,8 +284,10 @@ internal val JaStrings: Map = mapOf( "now_playing_song" to "再生中:%s", "fetch_songs_fail_or_net" to "データ異常またはネットワーク失敗", "explain_songs_data_kmp" to "曲データ:ライブラリメタデータ、バージョン", - "explain_user_data_kmp" to "ユーザーデータ:好き/履歴/プレイリスト/キャッシュを削除して終了", + "explain_image_cache" to "画像キャッシュ:ダウンロード済みカバー。削除後に再読み込み", + "explain_user_data_kmp" to "ユーザーデータ:好き/履歴/プレイリスト/キャッシュ", "remove_songs_data_kmp" to "曲データを削除", + "clear_image_cache" to "画像キャッシュを削除", "remove_user_data_kmp" to "ユーザーデータを削除", "confirm_delete_from_menu_short" to "プレイリストから削除しますか?", "play_text" to "放送中,停止しました", diff --git a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/ZhStrings.kt b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/ZhStrings.kt index a6803ef..e803368 100644 --- a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/ZhStrings.kt +++ b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/i18n/ZhStrings.kt @@ -284,8 +284,10 @@ internal val ZhStrings: Map = mapOf( "now_playing_song" to "正在播放:%s", "fetch_songs_fail_or_net" to "数据异常或网络失败", "explain_songs_data_kmp" to "歌曲数据:曲库元数据、版本号", - "explain_user_data_kmp" to "用户数据:喜欢/历史/歌单/缓存,并退出应用", + "explain_image_cache" to "图片缓存:已下载的封面,清除后重新加载", + "explain_user_data_kmp" to "用户数据:喜欢/历史/歌单/缓存", "remove_songs_data_kmp" to "清除歌曲数据", + "clear_image_cache" to "清除图片缓存", "remove_user_data_kmp" to "清除用户数据", "confirm_delete_from_menu_short" to "确认从歌单删除?", "play_text" to "播放中,已暂停", diff --git a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/SleepTimer.kt b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/SleepTimer.kt index 787c225..74e0716 100644 --- a/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/SleepTimer.kt +++ b/shared/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/SleepTimer.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds class SleepTimer( private val scope: CoroutineScope, @@ -23,7 +24,7 @@ class SleepTimer( _remainingMs.value = left job = scope.launch { while (left > 0) { - delay(1_000) + delay(1.seconds) left -= 1_000 _remainingMs.value = left.coerceAtLeast(0) } diff --git a/shared/src/iosMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/PlayerController.ios.kt b/shared/src/iosMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/PlayerController.ios.kt index 8cbb7e6..bdda829 100644 --- a/shared/src/iosMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/PlayerController.ios.kt +++ b/shared/src/iosMain/kotlin/com/zhushenwudi/lovelivemusicplayer/player/PlayerController.ios.kt @@ -1,6 +1,8 @@ package com.zhushenwudi.lovelivemusicplayer.player +import kotlinx.cinterop.CValue import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -40,9 +42,13 @@ import platform.AVFoundation.rate import platform.AVFoundation.replaceCurrentItemWithPlayerItem import platform.AVFoundation.resourceLoader import platform.AVFoundation.seekToTime +import platform.CoreGraphics.CGRectMake +import platform.CoreGraphics.CGSize +import platform.CoreGraphics.CGSizeMake import platform.CoreMedia.CMTimeGetSeconds import platform.CoreMedia.CMTimeMake import platform.Foundation.NSBundle +import platform.Foundation.NSThread import platform.Foundation.NSError import platform.Foundation.NSMutableURLRequest import platform.Foundation.NSNotification @@ -69,6 +75,9 @@ import platform.MediaPlayer.MPRemoteCommandHandlerStatus import platform.MediaPlayer.MPRemoteCommandHandlerStatusCommandFailed import platform.MediaPlayer.MPRemoteCommandHandlerStatusSuccess import platform.MediaPlayer.MPSeekCommandEvent +import platform.UIKit.UIGraphicsBeginImageContextWithOptions +import platform.UIKit.UIGraphicsEndImageContext +import platform.UIKit.UIGraphicsGetImageFromCurrentImageContext import platform.UIKit.UIImage import platform.darwin.dispatch_async import platform.darwin.dispatch_get_main_queue @@ -455,6 +464,10 @@ actual class PlayerController { * 封面 + 专辑信息启用灵动岛媒体胶囊。 */ private fun updateNowPlayingInfo() { + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue()) { updateNowPlayingInfo() } + return + } val s = _state.value val current = s.current ?: return val songName = current.musicName ?: return @@ -502,12 +515,32 @@ actual class PlayerController { else -> raw } val image = UIImage.imageWithContentsOfFile(path) ?: return null - val artwork = MPMediaItemArtwork(boundsSize = image.size) { _ -> image } + val artwork = coverArtwork(image) cachedArtworkMusicId = id cachedArtwork = artwork return artwork } + /** + * 系统会按锁屏 / 控制中心要的尺寸回调。直接交原图时,负一屏封面会空白。 + */ + private fun coverArtwork(image: UIImage): MPMediaItemArtwork { + return MPMediaItemArtwork(boundsSize = image.size) { requested -> + scaledCover(image, requested) + } + } + + private fun scaledCover(image: UIImage, requested: CValue): UIImage { + val width = requested.useContents { this.width } + val height = requested.useContents { this.height } + if (width < 1.0 || height < 1.0) return image + UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), false, 0.0) + image.drawInRect(CGRectMake(0.0, 0.0, width, height)) + val scaled = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return scaled ?: image + } + private fun prefetchHttpArtwork(musicId: String, url: String) { if (musicId == coverFetchingMusicId) return if (musicId == cachedArtworkMusicId && cachedArtwork != null) return @@ -537,7 +570,7 @@ actual class PlayerController { return@dispatch_async } if (_state.value.current?.musicId != musicId) return@dispatch_async - val artwork = MPMediaItemArtwork(boundsSize = image.size) { _ -> image } + val artwork = coverArtwork(image) cachedArtworkMusicId = musicId cachedArtwork = artwork updateNowPlayingInfo()