fix moegirl ad blocker and optimize func
This commit is contained in:
+52
@@ -0,0 +1,52 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.os.Build
|
||||
import android.view.WindowInsets
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
@Composable
|
||||
internal actual fun rememberStartupWindowMetrics(): StartupWindowMetrics {
|
||||
val view = LocalView.current
|
||||
return remember(view) { view.context.readStartupWindowMetrics() }
|
||||
}
|
||||
|
||||
private fun Context.readStartupWindowMetrics(): StartupWindowMetrics {
|
||||
val activity = findActivity() ?: return StartupWindowMetrics()
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
val metrics = activity.windowManager.currentWindowMetrics
|
||||
val mask = WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars()
|
||||
val bars = metrics.windowInsets.getInsets(mask)
|
||||
return StartupWindowMetrics(
|
||||
heightPx = metrics.bounds.height(),
|
||||
statusTopPx = bars.top,
|
||||
navBottomPx = bars.bottom,
|
||||
)
|
||||
}
|
||||
val root = ViewCompat.getRootWindowInsets(activity.window.decorView)
|
||||
val statusTop = root?.getInsets(WindowInsetsCompat.Type.statusBars())?.top
|
||||
?: activity.statusBarFallbackPx()
|
||||
val navBottom = root?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0
|
||||
return StartupWindowMetrics(
|
||||
heightPx = activity.resources.displayMetrics.heightPixels,
|
||||
statusTopPx = statusTop,
|
||||
navBottomPx = navBottom,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Activity.statusBarFallbackPx(): Int {
|
||||
val id = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
return if (id > 0) resources.getDimensionPixelSize(id) else 0
|
||||
}
|
||||
|
||||
private tailrec fun Context.findActivity(): Activity? = when (this) {
|
||||
is Activity -> this
|
||||
is ContextWrapper -> baseContext.findActivity()
|
||||
else -> null
|
||||
}
|
||||
+25
-8
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.zhushenwudi.lovelivemusicplayer.AppContainer.scope
|
||||
import com.zhushenwudi.lovelivemusicplayer.data.DataImportService
|
||||
import com.zhushenwudi.lovelivemusicplayer.db.LibraryRepository
|
||||
@@ -148,7 +149,6 @@ object AppContainer {
|
||||
// Android 通知封面用 file:// / https;iOS Now Playing 优先绝对路径。
|
||||
player.resolveCoverUri = { music ->
|
||||
pathResolver.absoluteCoverPath(music)
|
||||
?.takeIf { fileSystem.exists(it) }
|
||||
?: pathResolver.coverDisplayUri(music)
|
||||
}
|
||||
|
||||
@@ -181,7 +181,9 @@ object AppContainer {
|
||||
this.dataSync = DataSyncService(WsClient(http), library, scope)
|
||||
this.updateChecker = UpdateChecker(ossApi)
|
||||
this.sleepTimer = SleepTimer(scope) {
|
||||
player.pause()
|
||||
withContext(Dispatchers.Main) {
|
||||
runCatching { player.pause() }
|
||||
}
|
||||
}
|
||||
this.splashPhoto = SplashPhotoService(fileSystem, ossApi, scope)
|
||||
|
||||
@@ -195,7 +197,6 @@ object AppContainer {
|
||||
persistPlayQueue()
|
||||
}
|
||||
}
|
||||
refreshCarPlayCatalog()
|
||||
PlatformServices.desktopLyric.pipAutoOpen(settings.enableDesktopLyric)
|
||||
PlatformServices.carPlay.bind(PlatformServices.carPlayCatalog)
|
||||
bindCarPlayNowPlaying()
|
||||
@@ -206,6 +207,11 @@ object AppContainer {
|
||||
bindHistoryOnTrackChange()
|
||||
initSdStorage()
|
||||
bindUsbMountListener()
|
||||
// 全表快照放后台,并避开主页出来后的首屏解码。
|
||||
scope.launch {
|
||||
delay(2_000)
|
||||
refreshCarPlayCatalog()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,6 +473,8 @@ object AppContainer {
|
||||
private fun bindCarPlayNowPlaying() {
|
||||
carPlayNowPlayingJob?.cancel()
|
||||
carPlayNowPlayingJob = scope.launch {
|
||||
var recentKey: Pair<String?, Boolean>? = null
|
||||
var skipStartupRecent = true
|
||||
player.state
|
||||
.map {
|
||||
listOf(
|
||||
@@ -489,11 +497,20 @@ object AppContainer {
|
||||
favorite = current?.isLove == true,
|
||||
loopMode = player.state.value.loopMode,
|
||||
)
|
||||
val recent = library.findAllListByGroup(
|
||||
group = "all",
|
||||
enableHttp = settings.enableHttp,
|
||||
).recent.withSortOrder(settings.recentSortOrder)
|
||||
PlatformServices.carPlayCatalog.updateRecent(recent, cover)
|
||||
val key = current?.musicId to (current?.isLove == true)
|
||||
if (key != recentKey) {
|
||||
recentKey = key
|
||||
// 冷启动这一次交给延后的曲库快照,避免和首屏抢数据库。
|
||||
if (skipStartupRecent) {
|
||||
skipStartupRecent = false
|
||||
} else {
|
||||
val recent = library.recentVisible(
|
||||
group = "all",
|
||||
enableHttp = settings.enableHttp,
|
||||
).withSortOrder(settings.recentSortOrder)
|
||||
PlatformServices.carPlayCatalog.updateRecent(recent, cover)
|
||||
}
|
||||
}
|
||||
PlatformServices.carPlay.refreshNowPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
+74
-41
@@ -48,9 +48,12 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.min
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.zhushenwudi.lovelivemusicplayer.AppContainer
|
||||
import com.zhushenwudi.lovelivemusicplayer.Const
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.Album
|
||||
@@ -145,8 +148,9 @@ fun MainShell(
|
||||
val section = if (topTabIndex == 0) TopSection.LIBRARY else TopSection.MINE
|
||||
val weSlide = AppContainer.weSlide
|
||||
val weSlideFooter = AppContainer.weSlideFooter
|
||||
// 迷你播放器毛玻璃的 Haze 背景源
|
||||
// 迷你播放器毛玻璃的 Haze 背景源。首屏先不挂,避免整页离屏层和封面解码叠在一起。
|
||||
val miniPlayerHaze = rememberHazeState()
|
||||
var homeHazeEnabled by remember { mutableStateOf(false) }
|
||||
// 选择模式 > 0 时锁定歌库/我的标签与 WeSlide 手势
|
||||
val batchLock = selectMode == LibrarySelectMode.Batch
|
||||
val detailOpen = libraryDetail != null
|
||||
@@ -240,20 +244,14 @@ fun MainShell(
|
||||
}
|
||||
}
|
||||
|
||||
// 存储根/SD 路径变更后修复过期的 existFile(轻量 exists 扫描)。
|
||||
LaunchedEffect(Unit) {
|
||||
runCatching {
|
||||
AppContainer.library.rescanExistFiles(AppContainer.pathResolver)
|
||||
}
|
||||
refreshToken++
|
||||
}
|
||||
|
||||
// HTTP 关闭时按本地 existFile 过滤曲库。
|
||||
// 正倒序按页独立计算,不写进查询 key。
|
||||
val enableHttp = AppContainer.settings.enableHttp
|
||||
val historyRevision by AppContainer.historyRevision.collectAsState()
|
||||
val loveRevision by AppContainer.loveRevision.collectAsState()
|
||||
val libraryRevision by AppContainer.libraryRevision.collectAsState()
|
||||
val startupHistoryRevision = remember { historyRevision }
|
||||
val startupLoveRevision = remember { loveRevision }
|
||||
val libraryLists = remember(refreshToken, currentGroup, enableHttp, libraryRevision) {
|
||||
runCatching {
|
||||
AppContainer.library.findAllListByGroup(
|
||||
@@ -271,12 +269,12 @@ fun MainShell(
|
||||
)
|
||||
}
|
||||
}
|
||||
var music by remember { mutableStateOf(emptyList<Music>()) }
|
||||
var albums by remember { mutableStateOf(emptyList<Album>()) }
|
||||
var music by remember { mutableStateOf(libraryLists.music) }
|
||||
var albums by remember { mutableStateOf(libraryLists.albums) }
|
||||
var artists by remember { mutableStateOf(libraryLists.artists) }
|
||||
var menus by remember { mutableStateOf(libraryLists.menus) }
|
||||
var loved by remember { mutableStateOf(emptyList<Music>()) }
|
||||
var recent by remember { mutableStateOf(emptyList<Music>()) }
|
||||
var loved by remember { mutableStateOf(libraryLists.loved) }
|
||||
var recent by remember { mutableStateOf(libraryLists.recent) }
|
||||
LaunchedEffect(libraryLists) {
|
||||
music = libraryLists.music
|
||||
albums = libraryLists.albums
|
||||
@@ -285,35 +283,64 @@ fun MainShell(
|
||||
loved = libraryLists.loved
|
||||
recent = libraryLists.recent
|
||||
}
|
||||
// 播放历史更新时仅刷新最近播放列表。
|
||||
// 首屏封面先解码。本地文件核对延后,避免扫盘占满磁盘把列表拖卡。
|
||||
LaunchedEffect(Unit) {
|
||||
delay(1_500)
|
||||
homeHazeEnabled = true
|
||||
val groupAtStart = currentGroup.displayName
|
||||
val httpAtStart = enableHttp
|
||||
val changed = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
AppContainer.library.rescanExistFiles(AppContainer.pathResolver)
|
||||
}.getOrDefault(0)
|
||||
}
|
||||
if (changed > 0 &&
|
||||
currentGroup.displayName == groupAtStart &&
|
||||
enableHttp == httpAtStart
|
||||
) {
|
||||
val lists = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
AppContainer.library.findAllListByGroup(groupAtStart, httpAtStart)
|
||||
}.getOrNull()
|
||||
}
|
||||
if (lists != null &&
|
||||
currentGroup.displayName == groupAtStart &&
|
||||
enableHttp == httpAtStart
|
||||
) {
|
||||
music = lists.music
|
||||
albums = lists.albums
|
||||
artists = lists.artists
|
||||
menus = lists.menus
|
||||
loved = lists.loved
|
||||
recent = lists.recent
|
||||
}
|
||||
}
|
||||
}
|
||||
// 播放历史更新时仅刷新最近播放列表。启动时已经记过的版本不再查库。
|
||||
LaunchedEffect(historyRevision, currentGroup, enableHttp) {
|
||||
if (historyRevision == 0L) return@LaunchedEffect
|
||||
runCatching {
|
||||
AppContainer.library.findAllListByGroup(
|
||||
group = currentGroup.displayName,
|
||||
enableHttp = enableHttp,
|
||||
).recent
|
||||
}.getOrNull()?.let { recent = it }
|
||||
if (historyRevision == startupHistoryRevision) return@LaunchedEffect
|
||||
val groupName = currentGroup.displayName
|
||||
val http = enableHttp
|
||||
val next = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
AppContainer.library.recentVisible(groupName, http)
|
||||
}.getOrNull()
|
||||
}
|
||||
if (next != null) recent = next
|
||||
}
|
||||
// 收藏切换时刷新收藏列表。
|
||||
LaunchedEffect(loveRevision, currentGroup, enableHttp) {
|
||||
if (loveRevision == 0L) return@LaunchedEffect
|
||||
runCatching {
|
||||
AppContainer.library.findAllListByGroup(
|
||||
group = currentGroup.displayName,
|
||||
enableHttp = enableHttp,
|
||||
)
|
||||
}.getOrNull()?.let { lists ->
|
||||
loved = lists.loved
|
||||
// 同步歌曲/最近播放行的 isLove 与 Music.isLove 列。
|
||||
fun patch(list: List<Music>) = list.map { m ->
|
||||
val id = m.musicId ?: return@map m
|
||||
val love = AppContainer.library.getMusicById(id)?.isLove == true
|
||||
if (m.isLove == love) m else m.copy(isLove = love)
|
||||
}
|
||||
music = patch(music)
|
||||
recent = patch(recent)
|
||||
}
|
||||
if (loveRevision == startupLoveRevision) return@LaunchedEffect
|
||||
val groupName = currentGroup.displayName
|
||||
val http = enableHttp
|
||||
val lists = withContext(Dispatchers.Default) {
|
||||
runCatching {
|
||||
AppContainer.library.findAllListByGroup(groupName, http)
|
||||
}.getOrNull()
|
||||
} ?: return@LaunchedEffect
|
||||
loved = lists.loved
|
||||
music = lists.music
|
||||
recent = lists.recent
|
||||
}
|
||||
// 搜索模式:在歌曲/收藏/最近播放中按歌名过滤。
|
||||
val displayMusic = remember(music, songsSort, searchQuery, selectMode) {
|
||||
@@ -582,7 +609,7 @@ fun MainShell(
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides LayoutDirection.Ltr,
|
||||
LocalMiniPlayerHaze provides miniPlayerHaze,
|
||||
LocalMiniPlayerHaze provides if (homeHazeEnabled) miniPlayerHaze else null,
|
||||
LocalHomeListBottomInset provides HomeListChrome.ListBottomInset,
|
||||
) {
|
||||
WeSlide(
|
||||
@@ -617,7 +644,13 @@ fun MainShell(
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.hazeSource(state = miniPlayerHaze),
|
||||
.then(
|
||||
if (homeHazeEnabled) {
|
||||
Modifier.hazeSource(state = miniPlayerHaze)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -712,7 +745,7 @@ fun MainShell(
|
||||
state = contentPagerState,
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top,
|
||||
beyondViewportPageCount = 1,
|
||||
beyondViewportPageCount = 0,
|
||||
userScrollEnabled = !weSlide.isOpened &&
|
||||
!detailOpen &&
|
||||
selectMode == LibrarySelectMode.Normal,
|
||||
|
||||
+95
-7
@@ -1,22 +1,29 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.ui.player
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -27,13 +34,20 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.roundToInt
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.HazeTint
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
@@ -48,6 +62,7 @@ import com.zhushenwudi.lovelivemusicplayer.ui.icons.SvgAsset
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.TintIcon
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/** 迷你栏背后的主页列表内容,由 [com.zhushenwudi.lovelivemusicplayer.ui.MainShell] 提供。 */
|
||||
val LocalMiniPlayerHaze = compositionLocalOf<HazeState?> { null }
|
||||
@@ -132,15 +147,9 @@ fun MiniPlayerBar(
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
// 居中跑马灯歌名
|
||||
Text(
|
||||
MiniPlayerTitle(
|
||||
text = title,
|
||||
color = titleColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.then(
|
||||
@@ -205,6 +214,85 @@ fun MiniPlayerBar(
|
||||
}
|
||||
}
|
||||
|
||||
private val MiniTitleStyle = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
/** 略慢于常见 30dp/s 走马灯。 */
|
||||
private val MiniTitleMarqueeVelocity = 20.dp
|
||||
|
||||
private const val MiniTitleMarqueeHoldMs = 2_000
|
||||
|
||||
/**
|
||||
* 歌名宽于显示区域时走马灯:左侧对齐停 2 秒,匀速滚到右侧对齐再停 2 秒,
|
||||
* 然后跳回起点,再等 2 秒继续。未超出时居中静止。
|
||||
*/
|
||||
@Composable
|
||||
private fun MiniPlayerTitle(
|
||||
text: String,
|
||||
color: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val style = MiniTitleStyle.copy(color = color)
|
||||
BoxWithConstraints(
|
||||
modifier = modifier.clipToBounds(),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
val maxWidthPx = constraints.maxWidth
|
||||
val density = LocalDensity.current
|
||||
val textWidthPx = remember(text, style, textMeasurer, maxWidthPx) {
|
||||
textMeasurer.measure(
|
||||
text = text,
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Visible,
|
||||
).size.width
|
||||
}
|
||||
val overflowPx = (textWidthPx - maxWidthPx).coerceAtLeast(0)
|
||||
if (overflowPx <= 0) {
|
||||
Text(
|
||||
text = text,
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
val offsetX = remember(text) { Animatable(0f) }
|
||||
LaunchedEffect(text, overflowPx) {
|
||||
val velocityPxPerSec = with(density) { MiniTitleMarqueeVelocity.toPx() }
|
||||
val durationMs = (overflowPx / velocityPxPerSec * 1_000f)
|
||||
.toInt()
|
||||
.coerceAtLeast(1)
|
||||
offsetX.snapTo(0f)
|
||||
while (true) {
|
||||
delay(MiniTitleMarqueeHoldMs.toLong().milliseconds)
|
||||
offsetX.animateTo(
|
||||
targetValue = -overflowPx.toFloat(),
|
||||
animationSpec = tween(durationMs, easing = LinearEasing),
|
||||
)
|
||||
delay(MiniTitleMarqueeHoldMs.toLong().milliseconds)
|
||||
offsetX.snapTo(0f)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Visible,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth(unbounded = true, align = Alignment.Start)
|
||||
.offset { IntOffset(offsetX.value.roundToInt(), 0) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MiniBarIconButton(
|
||||
asset: SvgAsset,
|
||||
|
||||
+10
-2
@@ -196,7 +196,11 @@ fun SongList(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = listBottomInset),
|
||||
) {
|
||||
items(music) { item ->
|
||||
items(
|
||||
items = music,
|
||||
key = { it.musicId ?: "${it.musicName}-${it.musicPath}-${it.albumId}" },
|
||||
contentType = { "song" },
|
||||
) { item ->
|
||||
val id = item.musicId.orEmpty()
|
||||
SongListItem(
|
||||
music = item,
|
||||
@@ -267,7 +271,11 @@ private fun AlbumList(
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
items(albums) { album ->
|
||||
items(
|
||||
items = albums,
|
||||
key = { it.albumId ?: "${it.albumName}-${it.coverPath}" },
|
||||
contentType = { "album" },
|
||||
) { album ->
|
||||
AlbumGridItem(
|
||||
album = album,
|
||||
onClick = { album.albumId?.let(onOpen) },
|
||||
|
||||
+74
-24
@@ -1,12 +1,19 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.ui.theme
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.unit.Density
|
||||
@@ -24,31 +31,52 @@ val LocalDesignScale = staticCompositionLocalOf { 1f }
|
||||
/** [com.zhushenwudi.lovelivemusicplayer.ui.theme.ProvideDesignDensity] 缩放前的设备密度(供底部栏使用)。 */
|
||||
val LocalUnscaledDensity = staticCompositionLocalOf<Density?> { null }
|
||||
|
||||
/**
|
||||
* 首帧 [androidx.compose.ui.platform.LocalWindowInfo] 仍是 0×0、系统栏 inset 也还没派发时,
|
||||
* 用平台窗口度量顶上,避免先按 scale=1 铺满状态栏再缩回。
|
||||
*/
|
||||
data class StartupWindowMetrics(
|
||||
val heightPx: Int = 0,
|
||||
val statusTopPx: Int = 0,
|
||||
val navBottomPx: Int = 0,
|
||||
)
|
||||
|
||||
val LocalStartupWindowMetrics = staticCompositionLocalOf { StartupWindowMetrics() }
|
||||
|
||||
@Composable
|
||||
internal expect fun rememberStartupWindowMetrics(): StartupWindowMetrics
|
||||
|
||||
/**
|
||||
* 按设计稿比例缩放 [androidx.compose.ui.platform.LocalDensity],使 dp/sp 与设计尺寸一致。
|
||||
* 在应用根节点调用一次;底部栏需保持设备密度时用 [com.zhushenwudi.lovelivemusicplayer.ui.theme.ProvideUnscaledDensity] 包裹。
|
||||
*/
|
||||
@Composable
|
||||
fun ProvideDesignDensity(content: @Composable () -> Unit) {
|
||||
val base = LocalDensity.current
|
||||
val containerPx = LocalWindowInfo.current.containerSize
|
||||
val heightDp = containerPx.height / base.density
|
||||
// 按高度比例缩放;不低于设计尺寸;平板上限 1.35。忽略系统字体缩放(对齐 Flutter textScaler = 1.0)。
|
||||
val scale = remember(heightDp) {
|
||||
(heightDp / Const.UI_HEIGHT).coerceIn(1f, 1.35f)
|
||||
}
|
||||
val scaled = remember(base, scale) {
|
||||
Density(
|
||||
density = base.density * scale,
|
||||
fontScale = 1f,
|
||||
val startup = rememberStartupWindowMetrics()
|
||||
CompositionLocalProvider(LocalStartupWindowMetrics provides startup) {
|
||||
val base = LocalDensity.current
|
||||
val containerPx = LocalWindowInfo.current.containerSize
|
||||
val heightPx = if (containerPx.height > 0) containerPx.height else startup.heightPx
|
||||
if (heightPx <= 0) {
|
||||
Box(Modifier.fillMaxSize())
|
||||
return@CompositionLocalProvider
|
||||
}
|
||||
val heightDp = heightPx / base.density
|
||||
// 按高度比例缩放;不低于设计尺寸;平板上限 1.35。忽略系统字体缩放(对齐 Flutter textScaler = 1.0)。
|
||||
val scale = (heightDp / Const.UI_HEIGHT).coerceIn(1f, 1.35f)
|
||||
val scaled = remember(base.density, scale) {
|
||||
Density(
|
||||
density = base.density * scale,
|
||||
fontScale = 1f,
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides scaled,
|
||||
LocalUnscaledDensity provides base,
|
||||
LocalDesignScale provides scale,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides scaled,
|
||||
LocalUnscaledDensity provides base,
|
||||
LocalDesignScale provides scale,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
/** 恢复设备密度,保持底部栏物理尺寸不变。 */
|
||||
@@ -71,16 +99,38 @@ fun Dp.asUnscaledInScaledContext(): Dp {
|
||||
|
||||
/** 状态栏顶部内边距(dp,使用真实设备密度,忽略设计缩放)。 */
|
||||
@Composable
|
||||
fun unscaledStatusBarsTop(): Dp {
|
||||
val density = LocalUnscaledDensity.current ?: LocalDensity.current
|
||||
return with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
}
|
||||
fun unscaledStatusBarsTop(): Dp = unscaledSystemBarPx(
|
||||
composePx = WindowInsets.statusBars.getTop(systemBarDensity()),
|
||||
startupPx = LocalStartupWindowMetrics.current.statusTopPx,
|
||||
)
|
||||
|
||||
/** 导航栏底部内边距(dp,使用真实设备密度,忽略设计缩放)。 */
|
||||
@Composable
|
||||
fun unscaledNavigationBarsBottom(): Dp {
|
||||
val density = LocalUnscaledDensity.current ?: LocalDensity.current
|
||||
return with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||
fun unscaledNavigationBarsBottom(): Dp = unscaledSystemBarPx(
|
||||
composePx = WindowInsets.navigationBars.getBottom(systemBarDensity()),
|
||||
startupPx = LocalStartupWindowMetrics.current.navBottomPx,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun systemBarDensity(): Density =
|
||||
LocalUnscaledDensity.current ?: LocalDensity.current
|
||||
|
||||
/**
|
||||
* Compose 首帧系统栏 inset 常为 0。在它真正派发之前用启动时的窗口度量,
|
||||
* 派发之后以 Compose 为准(含沉浸式隐藏)。
|
||||
*/
|
||||
@Composable
|
||||
private fun unscaledSystemBarPx(composePx: Int, startupPx: Int): Dp {
|
||||
var dispatched by remember { mutableStateOf(false) }
|
||||
SideEffect {
|
||||
if (composePx > 0) dispatched = true
|
||||
}
|
||||
val px = when {
|
||||
composePx > 0 -> composePx
|
||||
dispatched -> 0
|
||||
else -> startupPx
|
||||
}
|
||||
return with(systemBarDensity()) { px.toDp() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.ui.theme
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
internal actual fun rememberStartupWindowMetrics(): StartupWindowMetrics = StartupWindowMetrics()
|
||||
Reference in New Issue
Block a user