fix photo cache and setting config
This commit is contained in:
+2
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
|
||||
+25
-4
@@ -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<LoadedLyric> = _playingLyric.asStateFlow()
|
||||
|
||||
@@ -78,6 +82,7 @@ object AppContainer {
|
||||
|
||||
/** 切存储清库后递增,刷新曲库 UI。 */
|
||||
val libraryRevision: StateFlow<Long> = _libraryRevision.asStateFlow()
|
||||
val shellReset: StateFlow<Long> = _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
|
||||
|
||||
+9
-1
@@ -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
|
||||
|
||||
+14
@@ -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,
|
||||
|
||||
+2
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 之前调用一次。
|
||||
|
||||
+87
-81
@@ -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<String>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
+40
-42
@@ -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
|
||||
|
||||
+3
-2
@@ -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()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[versions]
|
||||
agp = "9.3.1"
|
||||
agp = "9.4.1"
|
||||
android-compileSdk = "37"
|
||||
android-minSdk = "24"
|
||||
android-targetSdk = "37"
|
||||
|
||||
@@ -173,6 +173,6 @@
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+2
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,8 +284,10 @@ internal val DeStrings: Map<String, String> = 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",
|
||||
|
||||
@@ -284,8 +284,10 @@ internal val EnStrings: Map<String, String> = 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",
|
||||
|
||||
@@ -284,8 +284,10 @@ internal val JaStrings: Map<String, String> = 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 "放送中,停止しました",
|
||||
|
||||
@@ -284,8 +284,10 @@ internal val ZhStrings: Map<String, String> = 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 "播放中,已暂停",
|
||||
|
||||
+2
-1
@@ -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)
|
||||
}
|
||||
|
||||
+35
-2
@@ -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<CGSize>): 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()
|
||||
|
||||
Reference in New Issue
Block a user