adapt landscape direction
This commit is contained in:
+237
-72
@@ -18,7 +18,9 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
@@ -42,6 +44,13 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -78,6 +87,7 @@ import com.zhushenwudi.lovelivemusicplayer.theme.LocalDarkTheme
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.AddToMenuDialog
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.AppDrawer
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.BatchActionBar
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.LandscapeLibraryHeader
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.LibrarySelectMode
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.LlmpBottomBar
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.LoadingOverlay
|
||||
@@ -99,9 +109,11 @@ import com.zhushenwudi.lovelivemusicplayer.ui.scroll.StatusBarScrollToTopTap
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.scroll.rememberStatusBarScrollToTopAction
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.HomeListChrome
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalHomeListBottomInset
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.ProvideUnscaledDensity
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.asUnscaledInScaledContext
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.unscaledNavigationBarsBottom
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.unscaledSafeDrawingHorizontal
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.unscaledStatusBarsTop
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.weslide.WeSlide
|
||||
import kotlinx.coroutines.IO
|
||||
@@ -156,8 +168,10 @@ fun MainShell(
|
||||
// 选择模式 > 0 时锁定歌库/我的标签与 WeSlide 手势
|
||||
val batchLock = selectMode == LibrarySelectMode.Batch
|
||||
val detailOpen = libraryDetail != null
|
||||
LaunchedEffect(weSlide.isOpened, detailOpen) {
|
||||
if (weSlide.isOpened || detailOpen) {
|
||||
val isLandscape = LocalIsLandscape.current
|
||||
LaunchedEffect(weSlide.isOpened, detailOpen, isLandscape) {
|
||||
// 横屏详情仍保留底栏(三个入口 + 迷你条);全屏播放器展开时收起。
|
||||
if (weSlide.isOpened || (detailOpen && !isLandscape)) {
|
||||
weSlideFooter.hide()
|
||||
} else {
|
||||
weSlideFooter.show()
|
||||
@@ -500,6 +514,14 @@ fun MainShell(
|
||||
}
|
||||
|
||||
val shellBg = if (dark) ColorMs.NightPrimary else ColorMs.LightPrimary
|
||||
val focusManager = LocalFocusManager.current
|
||||
val searchCoords = remember { mutableStateOf<LayoutCoordinates?>(null) }
|
||||
var searchShellCoords by remember { mutableStateOf<LayoutCoordinates?>(null) }
|
||||
val landscapeSearchVisible = selectMode != LibrarySelectMode.Batch &&
|
||||
libraryTab != LibraryTab.ALBUMS &&
|
||||
libraryTab != LibraryTab.ARTISTS &&
|
||||
libraryTab != LibraryTab.MENUS
|
||||
val (safeStart, safeEnd) = unscaledSafeDrawingHorizontal()
|
||||
val footerNavInset = unscaledNavigationBarsBottom()
|
||||
val footerHeightUnscaled = when {
|
||||
selectMode == LibrarySelectMode.Batch && batchSelectable -> 102.dp + footerNavInset
|
||||
@@ -507,7 +529,12 @@ fun MainShell(
|
||||
}
|
||||
val miniPlayerHeight = HomeListChrome.MiniPlayerHeight
|
||||
val footerHeight = footerHeightUnscaled.asUnscaledInScaledContext()
|
||||
val panelMinSize = miniPlayerHeight + HomeListChrome.MiniPlayerFooterGap + footerHeight
|
||||
// 横屏迷你条在底栏右侧,收起时面板只露出与底栏等高的一条,由底栏盖住。
|
||||
val panelMinSize = if (isLandscape) {
|
||||
footerHeight
|
||||
} else {
|
||||
miniPlayerHeight + HomeListChrome.MiniPlayerFooterGap + footerHeight
|
||||
}
|
||||
val statusBarTop = unscaledStatusBarsTop().asUnscaledInScaledContext()
|
||||
|
||||
fun importData(force: Boolean) {
|
||||
@@ -584,8 +611,13 @@ fun MainShell(
|
||||
drawerContent = {
|
||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||
BoxWithConstraints(Modifier.fillMaxHeight()) {
|
||||
// 与原项目 endDrawer:min(0.35 * 屏高, 屏宽),不用 Material 默认 240–360dp。
|
||||
val drawerWidth = min(maxHeight * 0.35f, maxWidth)
|
||||
// 竖屏与原项目 endDrawer:min(0.35 * 屏高, 屏宽)。
|
||||
// 横屏屏高变矮,改用约 300dp,且不超过屏宽的 45%。
|
||||
val drawerWidth = if (isLandscape) {
|
||||
min(300.dp, maxWidth * 0.45f)
|
||||
} else {
|
||||
min(maxHeight * 0.35f, maxWidth)
|
||||
}
|
||||
Surface(
|
||||
color = if (dark) ColorMs.NightPrimary else ColorMs.White,
|
||||
modifier = Modifier
|
||||
@@ -618,9 +650,42 @@ fun MainShell(
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides LayoutDirection.Ltr,
|
||||
LocalMiniPlayerHaze provides if (homeHazeEnabled) miniPlayerHaze else null,
|
||||
LocalHomeListBottomInset provides HomeListChrome.ListBottomInset,
|
||||
LocalHomeListBottomInset provides if (isLandscape) {
|
||||
16.dp
|
||||
} else {
|
||||
HomeListChrome.ListBottomInset
|
||||
},
|
||||
) {
|
||||
WeSlide(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.onGloballyPositioned { shellCoords ->
|
||||
searchShellCoords = shellCoords
|
||||
}
|
||||
.pointerInput(focusManager) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||
val down = event.changes.firstOrNull {
|
||||
it.changedToDownIgnoreConsumed()
|
||||
} ?: continue
|
||||
val root = searchShellCoords
|
||||
val field = searchCoords.value
|
||||
val inside = root != null &&
|
||||
field != null &&
|
||||
root.isAttached &&
|
||||
field.isAttached &&
|
||||
run {
|
||||
val local = field.localPositionOf(root, down.position)
|
||||
local.x >= 0f &&
|
||||
local.y >= 0f &&
|
||||
local.x <= field.size.width &&
|
||||
local.y <= field.size.height
|
||||
}
|
||||
if (!inside) focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
},
|
||||
controller = weSlide,
|
||||
footerController = weSlideFooter,
|
||||
panelMinSize = panelMinSize,
|
||||
@@ -664,8 +729,49 @@ fun MainShell(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(shellBg)
|
||||
.padding(top = statusBarTop),
|
||||
.padding(top = statusBarTop)
|
||||
.then(
|
||||
if (isLandscape) {
|
||||
Modifier.padding(
|
||||
start = safeStart + 20.dp,
|
||||
end = safeEnd + 20.dp,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
if (isLandscape) {
|
||||
LandscapeLibraryHeader(
|
||||
section = section,
|
||||
onSection = { next ->
|
||||
topTabIndex =
|
||||
if (next == TopSection.LIBRARY) 0 else 1
|
||||
},
|
||||
interactionEnabled = !batchLock && !detailOpen,
|
||||
query = searchDraft,
|
||||
onQueryChange = { next ->
|
||||
searchDraft = next
|
||||
searchQuery = next
|
||||
if (selectMode != LibrarySelectMode.Batch) {
|
||||
selectMode = if (next.isBlank()) {
|
||||
LibrarySelectMode.Normal
|
||||
} else {
|
||||
LibrarySelectMode.Search
|
||||
}
|
||||
}
|
||||
},
|
||||
group = currentGroup,
|
||||
onOpenDrawer = if (batchLock || detailOpen) {
|
||||
null
|
||||
} else {
|
||||
{ scope.launch { drawerState.open() } }
|
||||
},
|
||||
shellBg = shellBg,
|
||||
showSearch = landscapeSearchVisible,
|
||||
onSearchPlaced = { searchCoords.value = it },
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -704,6 +810,7 @@ fun MainShell(
|
||||
hasShadow = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (libraryTab != LibraryTab.ARTISTS &&
|
||||
libraryTab != LibraryTab.MENUS
|
||||
@@ -711,7 +818,15 @@ fun MainShell(
|
||||
SongLibraryTopBar(
|
||||
countLabel = countLabel,
|
||||
libraryTab = libraryTab,
|
||||
selectMode = selectMode,
|
||||
selectMode = if (
|
||||
isLandscape &&
|
||||
selectMode == LibrarySelectMode.Search
|
||||
) {
|
||||
LibrarySelectMode.Normal
|
||||
} else {
|
||||
selectMode
|
||||
},
|
||||
showSearch = !isLandscape,
|
||||
sortAsc = currentSortAsc,
|
||||
searchQuery = searchDraft,
|
||||
checkedCount = checkedIds.size,
|
||||
@@ -797,64 +912,16 @@ fun MainShell(
|
||||
}
|
||||
|
||||
// 进入:右滑;退出:页面渐隐 + 封面 Hero(零位移保持转场时长)。
|
||||
AnimatedVisibility(
|
||||
// 抽到独立函数,避免外层 Row 把 AnimatedVisibility 解析成行内重载。
|
||||
LibraryDetailOverlay(
|
||||
visible = detailOpen,
|
||||
enter = slideInHorizontally(
|
||||
animationSpec = tween(
|
||||
LlmpTransitionMs,
|
||||
easing = EaseOutQuad,
|
||||
),
|
||||
initialOffsetX = { it },
|
||||
) + fadeIn(
|
||||
animationSpec = tween(
|
||||
LlmpTransitionMs,
|
||||
easing = EaseOutQuad,
|
||||
),
|
||||
),
|
||||
exit = slideOutHorizontally(
|
||||
animationSpec = tween(
|
||||
LlmpTransitionMs,
|
||||
easing = EaseOutQuad,
|
||||
),
|
||||
targetOffsetX = { 0 },
|
||||
),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalNavAnimatedVisibilityScope provides this,
|
||||
) {
|
||||
when (val detail = retainedDetail) {
|
||||
is LibraryDetail.Album -> AlbumDetailScreen(
|
||||
albumId = detail.albumId,
|
||||
onBack = ::closeLibraryDetail,
|
||||
onOpenPlayer = {
|
||||
closeLibraryDetail()
|
||||
weSlide.show()
|
||||
},
|
||||
)
|
||||
|
||||
is LibraryDetail.Artist -> ArtistDetailScreen(
|
||||
artistId = detail.artistId,
|
||||
onBack = ::closeLibraryDetail,
|
||||
onOpenPlayer = {
|
||||
closeLibraryDetail()
|
||||
weSlide.show()
|
||||
},
|
||||
)
|
||||
|
||||
is LibraryDetail.Menu -> MenuDetailScreen(
|
||||
menuId = detail.menuId,
|
||||
onBack = ::closeLibraryDetail,
|
||||
onOpenPlayer = {
|
||||
closeLibraryDetail()
|
||||
weSlide.show()
|
||||
},
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
detail = retainedDetail,
|
||||
onClose = ::closeLibraryDetail,
|
||||
onOpenPlayer = {
|
||||
closeLibraryDetail()
|
||||
weSlide.show()
|
||||
},
|
||||
)
|
||||
|
||||
// 点击状态栏将当前曲库列表滚回顶部。
|
||||
if (!detailOpen) {
|
||||
@@ -870,11 +937,15 @@ fun MainShell(
|
||||
}
|
||||
}
|
||||
},
|
||||
panelHeader = {
|
||||
MiniPlayerBar(
|
||||
onOpenPlayer = { weSlide.show() },
|
||||
interactionEnabled = !batchLock,
|
||||
)
|
||||
panelHeader = if (isLandscape) {
|
||||
null
|
||||
} else {
|
||||
{
|
||||
MiniPlayerBar(
|
||||
onOpenPlayer = { weSlide.show() },
|
||||
interactionEnabled = !batchLock,
|
||||
)
|
||||
}
|
||||
},
|
||||
panel = {
|
||||
FullPlayerScreen(
|
||||
@@ -884,8 +955,8 @@ fun MainShell(
|
||||
},
|
||||
footer = {
|
||||
ProvideUnscaledDensity {
|
||||
if (detailOpen) {
|
||||
// 详情页不画底栏,只保留槽位高度动画给迷你条下移。
|
||||
if (detailOpen && !isLandscape) {
|
||||
// 竖屏详情不画底栏,只保留槽位高度动画给迷你条下移。
|
||||
} else if (selectMode == LibrarySelectMode.Batch && batchSelectable) {
|
||||
BatchActionBar(
|
||||
showUnlove = libraryTab == LibraryTab.LOVE,
|
||||
@@ -897,6 +968,54 @@ fun MainShell(
|
||||
if (selectedMusic().isNotEmpty()) showUnloveConfirm = true
|
||||
},
|
||||
)
|
||||
} else if (isLandscape) {
|
||||
val barBg = if (dark) {
|
||||
ColorMs.BottomBarDark
|
||||
} else {
|
||||
ColorMs.BottomBarLight
|
||||
}
|
||||
val barDensity = LocalDensity.current
|
||||
val barDirection = LocalLayoutDirection.current
|
||||
val barSafeStart = with(barDensity) {
|
||||
WindowInsets.safeDrawing.getLeft(this, barDirection).toDp()
|
||||
}
|
||||
val barSafeEnd = with(barDensity) {
|
||||
WindowInsets.safeDrawing.getRight(this, barDirection).toDp()
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(barBg)
|
||||
.padding(
|
||||
start = barSafeStart + 20.dp,
|
||||
end = barSafeEnd + 20.dp,
|
||||
),
|
||||
) {
|
||||
LlmpBottomBar(
|
||||
modifier = Modifier.weight(1.15f).fillMaxHeight(),
|
||||
section = section,
|
||||
selectedIndex = bottomIndex,
|
||||
horizontalLabels = true,
|
||||
onSelect = { index ->
|
||||
bottomIndex = index
|
||||
},
|
||||
onSectionChange = { next ->
|
||||
val top = if (next == TopSection.LIBRARY) 0 else 1
|
||||
if (topTabIndex != top) {
|
||||
topTabIndex = top
|
||||
}
|
||||
},
|
||||
)
|
||||
MiniPlayerBar(
|
||||
onOpenPlayer = {
|
||||
if (detailOpen) closeLibraryDetail()
|
||||
weSlide.show()
|
||||
},
|
||||
interactionEnabled = !batchLock,
|
||||
embedded = true,
|
||||
modifier = Modifier.weight(0.85f).fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LlmpBottomBar(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@@ -983,3 +1102,49 @@ fun MainShell(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
private fun LibraryDetailOverlay(
|
||||
visible: Boolean,
|
||||
detail: LibraryDetail?,
|
||||
onClose: () -> Unit,
|
||||
onOpenPlayer: () -> Unit,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = slideInHorizontally(
|
||||
animationSpec = tween(LlmpTransitionMs, easing = EaseOutQuad),
|
||||
initialOffsetX = { it },
|
||||
) + fadeIn(animationSpec = tween(LlmpTransitionMs, easing = EaseOutQuad)),
|
||||
exit = slideOutHorizontally(
|
||||
animationSpec = tween(LlmpTransitionMs, easing = EaseOutQuad),
|
||||
targetOffsetX = { 0 },
|
||||
),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
CompositionLocalProvider(LocalNavAnimatedVisibilityScope provides this) {
|
||||
when (detail) {
|
||||
is LibraryDetail.Album -> AlbumDetailScreen(
|
||||
albumId = detail.albumId,
|
||||
onBack = onClose,
|
||||
onOpenPlayer = onOpenPlayer,
|
||||
)
|
||||
|
||||
is LibraryDetail.Artist -> ArtistDetailScreen(
|
||||
artistId = detail.artistId,
|
||||
onBack = onClose,
|
||||
onOpenPlayer = onOpenPlayer,
|
||||
)
|
||||
|
||||
is LibraryDetail.Menu -> MenuDetailScreen(
|
||||
menuId = detail.menuId,
|
||||
onBack = onClose,
|
||||
onOpenPlayer = onOpenPlayer,
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+59
-32
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material3.Text
|
||||
@@ -48,6 +49,8 @@ fun LlmpBottomBar(
|
||||
onSelect: (Int) -> Unit,
|
||||
onSectionChange: (TopSection) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
/** 横屏底栏宽度够,图标和文字左右排列。 */
|
||||
horizontalLabels: Boolean = false,
|
||||
) {
|
||||
val dark = LocalDarkTheme.current
|
||||
val tabStripHeight = HomeListChrome.BottomBarContentHeight
|
||||
@@ -99,38 +102,62 @@ fun LlmpBottomBar(
|
||||
) {
|
||||
tabs.forEachIndexed { index, tab ->
|
||||
val selected = pageSection == section && index == selectedIndex
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
if (pageSection != section) {
|
||||
onSectionChange(pageSection)
|
||||
}
|
||||
onSelect(index)
|
||||
},
|
||||
) {
|
||||
TintIcon(
|
||||
asset = tab.icon,
|
||||
tint = if (selected) ColorMs.PinkF940A7 else ColorMs.UnselectedIcon,
|
||||
size = 18.dp,
|
||||
contentDescription = tr(tab.titleKey),
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = tr(tab.titleKey),
|
||||
fontSize = 10.sp,
|
||||
color = if (selected) {
|
||||
ColorMs.BottomSelectedText
|
||||
} else {
|
||||
ColorMs.TabUnselected.copy(alpha = 0.5f)
|
||||
},
|
||||
)
|
||||
val tint = if (selected) ColorMs.PinkF940A7 else ColorMs.UnselectedIcon
|
||||
val labelColor = if (selected) {
|
||||
ColorMs.BottomSelectedText
|
||||
} else {
|
||||
ColorMs.TabUnselected.copy(alpha = 0.5f)
|
||||
}
|
||||
val itemModifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
if (pageSection != section) {
|
||||
onSectionChange(pageSection)
|
||||
}
|
||||
onSelect(index)
|
||||
}
|
||||
if (horizontalLabels) {
|
||||
Row(
|
||||
modifier = itemModifier,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TintIcon(
|
||||
asset = tab.icon,
|
||||
tint = tint,
|
||||
size = 18.dp,
|
||||
contentDescription = tr(tab.titleKey),
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(
|
||||
text = tr(tab.titleKey),
|
||||
fontSize = 13.sp,
|
||||
color = labelColor,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = itemModifier,
|
||||
) {
|
||||
TintIcon(
|
||||
asset = tab.icon,
|
||||
tint = tint,
|
||||
size = 18.dp,
|
||||
contentDescription = tr(tab.titleKey),
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = tr(tab.titleKey),
|
||||
fontSize = 10.sp,
|
||||
color = labelColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+136
@@ -8,10 +8,12 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -21,20 +23,25 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.unscaledSafeDrawingHorizontal
|
||||
import com.zhushenwudi.lovelivemusicplayer.AppContainer
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.Music
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||
@@ -49,6 +56,9 @@ import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
||||
/** 对齐 Flutter 末项 `95.h`:只挡住迷你播放器,不再叠加已收起的底栏高度。 */
|
||||
private val DetailsListBottomInset = 95.dp
|
||||
|
||||
/** 详情封面边长。竖屏 240;横屏按左侧栏剩余高度缩小。 */
|
||||
val LocalDetailCoverSize = staticCompositionLocalOf { 240.dp }
|
||||
|
||||
/**
|
||||
* 详情页:顶栏、封面、粘性播放栏、歌曲列表。
|
||||
*/
|
||||
@@ -145,6 +155,38 @@ fun DetailsPage(
|
||||
.background(scaffoldBg)
|
||||
.graphicsLayer { alpha = bodyAlpha },
|
||||
)
|
||||
if (LocalIsLandscape.current) {
|
||||
LandscapeDetailColumns(
|
||||
title = title,
|
||||
songs = songs,
|
||||
onBack = onBack,
|
||||
bodyFade = bodyFade,
|
||||
scaffoldBg = scaffoldBg,
|
||||
selectMode = selectMode,
|
||||
checkedIds = checkedIds,
|
||||
playingId = playingId,
|
||||
onPlayAll = { playQueue() },
|
||||
onOpenSelect = { selectMode = true },
|
||||
onSelectAll = { checked ->
|
||||
checkedIds = if (checked) {
|
||||
songs.mapNotNull { it.musicId }.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
},
|
||||
onCancelSelect = ::closeSelect,
|
||||
onPlay = { playQueue(it) },
|
||||
onPlayNext = { item ->
|
||||
addMusicPlayNext(item)
|
||||
PlatformUi.showToast(I18n.t("add_success"))
|
||||
},
|
||||
onMore = { moreMusic = it },
|
||||
onCheckedChange = { id, checked ->
|
||||
checkedIds = if (checked) checkedIds + id else checkedIds - id
|
||||
},
|
||||
cover = cover,
|
||||
)
|
||||
} else {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
AppHeader(title = title, onBack = onBack, modifier = bodyFade)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
@@ -213,6 +255,7 @@ fun DetailsPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectMode) {
|
||||
BatchActionBar(
|
||||
@@ -280,6 +323,99 @@ fun DetailsPage(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LandscapeDetailColumns(
|
||||
title: String,
|
||||
songs: List<Music>,
|
||||
onBack: () -> Unit,
|
||||
bodyFade: Modifier,
|
||||
scaffoldBg: Color,
|
||||
selectMode: Boolean,
|
||||
checkedIds: Set<String>,
|
||||
playingId: String?,
|
||||
onPlayAll: () -> Unit,
|
||||
onOpenSelect: () -> Unit,
|
||||
onSelectAll: (Boolean) -> Unit,
|
||||
onCancelSelect: () -> Unit,
|
||||
onPlay: (Music) -> Unit,
|
||||
onPlayNext: (Music) -> Unit,
|
||||
onMore: (Music) -> Unit,
|
||||
onCheckedChange: (String, Boolean) -> Unit,
|
||||
cover: @Composable () -> Unit,
|
||||
) {
|
||||
val (safeStart, safeEnd) = unscaledSafeDrawingHorizontal()
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = safeStart + 16.dp, end = safeEnd + 16.dp),
|
||||
) {
|
||||
AppHeader(title = title, onBack = onBack, modifier = bodyFade)
|
||||
Row(Modifier.weight(1f).fillMaxWidth()) {
|
||||
Column(
|
||||
Modifier
|
||||
.width(240.dp)
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val available = (maxHeight - 80.dp).coerceAtLeast(64.dp)
|
||||
val coverSize = maxWidth.coerceAtMost(available).coerceAtMost(240.dp)
|
||||
CompositionLocalProvider(LocalDetailCoverSize provides coverSize) {
|
||||
cover()
|
||||
}
|
||||
}
|
||||
DetailsListTop(
|
||||
itemsLength = songs.size,
|
||||
selectMode = selectMode,
|
||||
selectAll = songs.isNotEmpty() &&
|
||||
songs.mapNotNull { it.musicId }.all { it in checkedIds },
|
||||
checkedCount = checkedIds.size,
|
||||
onPlayTap = onPlayAll,
|
||||
onFunctionTap = onOpenSelect,
|
||||
onSelectAllTap = onSelectAll,
|
||||
onCancelTap = onCancelSelect,
|
||||
pinnedBg = scaffoldBg,
|
||||
modifier = bodyFade,
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f).fillMaxHeight(),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = if (selectMode) 120.dp else 12.dp,
|
||||
),
|
||||
) {
|
||||
itemsIndexed(
|
||||
songs,
|
||||
key = { _, m -> m.musicId ?: m.hashCode().toString() },
|
||||
) { index, item ->
|
||||
val id = item.musicId.orEmpty()
|
||||
SongListItem(
|
||||
music = item,
|
||||
isPlaying = id.isNotEmpty() && id == playingId,
|
||||
onClick = { onPlay(item) },
|
||||
onPlayNext = { onPlayNext(item) },
|
||||
onMore = { onMore(item) },
|
||||
selectMode = selectMode,
|
||||
checked = id.isNotEmpty() && id in checkedIds,
|
||||
onCheckedChange = { checked ->
|
||||
if (id.isNotEmpty()) onCheckedChange(id, checked)
|
||||
},
|
||||
modifier = bodyFade.padding(
|
||||
bottom = if (index == songs.lastIndex) 0.dp else 10.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 详情顶栏 — 播放全部/选择栏,粘性定位。 */
|
||||
@Composable
|
||||
fun DetailsListTop(
|
||||
|
||||
+9
-2
@@ -52,6 +52,7 @@ import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
||||
import com.zhushenwudi.lovelivemusicplayer.theme.ColorMs
|
||||
import com.zhushenwudi.lovelivemusicplayer.theme.LocalDarkTheme
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||
|
||||
/**
|
||||
* 双按钮对话框 — 对齐 Flutter [TwoButtonDialog]。
|
||||
@@ -360,8 +361,14 @@ private fun LlmpDialogShell(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
// 记住首次布局宽度,避免键盘弹出时 adjustResize 缩小 maxHeight 导致弹窗变窄。
|
||||
val dialogWidth = remember {
|
||||
min(maxHeight * 0.4f, maxWidth * 0.8f)
|
||||
// 横屏不用屏高系数,否则弹窗会被压成一条。
|
||||
val isLandscape = LocalIsLandscape.current
|
||||
val dialogWidth = remember(isLandscape) {
|
||||
if (isLandscape) {
|
||||
min(maxWidth * 0.72f, 420.dp)
|
||||
} else {
|
||||
min(maxHeight * 0.4f, maxWidth * 0.8f)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
+139
-1
@@ -8,29 +8,37 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.GroupKey
|
||||
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
||||
import com.zhushenwudi.lovelivemusicplayer.navigation.LibraryTab
|
||||
import com.zhushenwudi.lovelivemusicplayer.navigation.TopSection
|
||||
@@ -39,6 +47,7 @@ import com.zhushenwudi.lovelivemusicplayer.theme.LocalDarkTheme
|
||||
import com.zhushenwudi.lovelivemusicplayer.theme.katongFontFamily
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.SvgAsset
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.TintIcon
|
||||
|
||||
@Composable
|
||||
fun TopLibraryTabs(
|
||||
@@ -95,6 +104,132 @@ fun TopLibraryTabs(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 横屏顶栏:左侧歌库/我的,右侧搜索条和抽屉按钮。
|
||||
*/
|
||||
@Composable
|
||||
fun LandscapeLibraryHeader(
|
||||
section: TopSection,
|
||||
onSection: (TopSection) -> Unit,
|
||||
interactionEnabled: Boolean,
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
group: GroupKey,
|
||||
onOpenDrawer: (() -> Unit)?,
|
||||
shellBg: Color,
|
||||
showSearch: Boolean,
|
||||
onSearchPlaced: (LayoutCoordinates?) -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val dark = LocalDarkTheme.current
|
||||
val hint = if (dark) ColorMs.GrayDFD else ColorMs.Gray999
|
||||
val fieldBg = if (dark) {
|
||||
ColorMs.White.copy(alpha = 0.08f)
|
||||
} else {
|
||||
ColorMs.Text333.copy(alpha = 0.06f)
|
||||
}
|
||||
val textColor = if (dark) ColorMs.White else ColorMs.Text333
|
||||
DisposableEffect(showSearch) {
|
||||
if (!showSearch) onSearchPlaced(null)
|
||||
onDispose { onSearchPlaced(null) }
|
||||
}
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TopLibraryTabs(
|
||||
selected = section,
|
||||
onSelect = onSection,
|
||||
interactionEnabled = interactionEnabled,
|
||||
)
|
||||
Box(
|
||||
Modifier.weight(1f),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
if (showSearch) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 16.dp)
|
||||
.widthIn(max = 360.dp)
|
||||
.fillMaxWidth()
|
||||
.height(36.dp)
|
||||
.onGloballyPositioned { onSearchPlaced(it) }
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(fieldBg)
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TintIcon(
|
||||
asset = LlmpIcons.Search,
|
||||
tint = hint,
|
||||
size = 16.dp,
|
||||
contentDescription = tr("search"),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight(),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
if (query.isEmpty()) {
|
||||
Text(
|
||||
tr("input_song_keyword"),
|
||||
color = hint,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
BasicTextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
enabled = interactionEnabled,
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(
|
||||
color = textColor,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 14.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.Both,
|
||||
),
|
||||
),
|
||||
cursorBrush = SolidColor(ColorMs.PinkF940A7),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { onQueryChange(query) }),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
if (query.isNotEmpty() && interactionEnabled) {
|
||||
Text(
|
||||
"✕",
|
||||
color = hint,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onQueryChange("") },
|
||||
)
|
||||
.padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(12.dp))
|
||||
NeumorphicGroupLogoButton(
|
||||
group = group,
|
||||
onClick = onOpenDrawer,
|
||||
size = 36.dp,
|
||||
background = if (group.isDefaultGroupLogo()) Color(0x4DFFAE00) else shellBg,
|
||||
contentPadding = if (group.isDefaultGroupLogo()) 3.dp else 0.dp,
|
||||
hasShadow = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val FilterIcon = SvgAsset("drawable/main_ic_function.svg")
|
||||
|
||||
/** 选择模式:0 正常,1 批量,2 搜索。 */
|
||||
@@ -119,6 +254,7 @@ fun SongLibraryTopBar(
|
||||
onSelectAll: (Boolean) -> Unit,
|
||||
onBatchFinish: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showSearch: Boolean = true,
|
||||
) {
|
||||
when (selectMode) {
|
||||
LibrarySelectMode.Batch -> BatchTopBar(
|
||||
@@ -145,6 +281,7 @@ fun SongLibraryTopBar(
|
||||
onSearchOpen = onSearchOpen,
|
||||
onSort = onSort,
|
||||
onBatchOpen = onBatchOpen,
|
||||
showSearch = showSearch,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -159,6 +296,7 @@ private fun NormalTopBar(
|
||||
onSearchOpen: () -> Unit,
|
||||
onSort: () -> Unit,
|
||||
onBatchOpen: () -> Unit,
|
||||
showSearch: Boolean = true,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val dark = LocalDarkTheme.current
|
||||
@@ -180,7 +318,7 @@ private fun NormalTopBar(
|
||||
maxLines = 1,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (!isAlbum) {
|
||||
if (!isAlbum && showSearch) {
|
||||
NeumorphicIconButton(
|
||||
icon = LlmpIcons.Search,
|
||||
onClick = onSearchOpen,
|
||||
|
||||
+315
-9
@@ -68,6 +68,7 @@ import androidx.compose.ui.text.TextStyle
|
||||
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.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -93,6 +94,8 @@ import com.zhushenwudi.lovelivemusicplayer.ui.components.NeumorphicSurface
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.PlayerMusicCover
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.rememberMusicCoverUri
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.unscaledSafeDrawingHorizontal
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToLong
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
@@ -164,9 +167,10 @@ fun FullPlayerScreen(
|
||||
if (panelOpen) content = PlayerContent.Cover
|
||||
}
|
||||
// 歌词列表:禁用 WeSlide 嵌套/面板拖拽,避免滚动被抢占。
|
||||
// 歌词区与进度条之间的顶栏仍通过 onChromeVerticalDrag 驱动关闭。
|
||||
LaunchedEffect(content) {
|
||||
AppContainer.weSlide.dragEnabled = content == PlayerContent.Cover
|
||||
// 横屏右侧始终是歌词,拖拽只留在左侧封面区。
|
||||
val landscape = LocalIsLandscape.current
|
||||
LaunchedEffect(content, landscape) {
|
||||
AppContainer.weSlide.dragEnabled = !landscape && content == PlayerContent.Cover
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { AppContainer.weSlide.dragEnabled = true }
|
||||
@@ -190,6 +194,42 @@ fun FullPlayerScreen(
|
||||
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Box(Modifier.fillMaxSize().background(bg))
|
||||
if (landscape) {
|
||||
LandscapePlayerBody(
|
||||
current = current,
|
||||
isPlaying = isPlaying,
|
||||
loopMode = loopMode,
|
||||
lyric = lyric,
|
||||
lrcType = lrcType,
|
||||
isLove = isLove,
|
||||
dark = dark,
|
||||
iconTint = iconTint,
|
||||
controlBg = controlBg,
|
||||
thumbColor = thumbColor,
|
||||
activeTrack = activeTrack,
|
||||
inactiveTrack = inactiveTrack,
|
||||
timeColor = timeColor,
|
||||
lyricPlayingColor = lyricPlayingColor,
|
||||
lyricOtherColor = lyricOtherColor,
|
||||
listState = listState,
|
||||
onBack = onBack,
|
||||
onMore = { if (current != null) showMore = true },
|
||||
onToggleLove = {
|
||||
current?.musicId?.let { id ->
|
||||
isLove = AppContainer.toggleLove(id)
|
||||
}
|
||||
},
|
||||
onAddSong = {
|
||||
if (current?.musicId != null) showAddToMenu = true
|
||||
},
|
||||
onToggleTranslate = {
|
||||
AppContainer.lyricService.toggleType()
|
||||
lrcType = AppContainer.lyricService.lrcType
|
||||
lyric = AppContainer.applyLyricDisplayType()
|
||||
},
|
||||
onShowPlaylist = { showPlaylist = true },
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -361,7 +401,7 @@ fun FullPlayerScreen(
|
||||
background = controlBg,
|
||||
)
|
||||
NeumorphicIconButton(
|
||||
icon = if (isPlaying) LlmpIcons.Pause else LlmpIcons.Play,
|
||||
icon = if (isPlaying && current != null) LlmpIcons.Pause else LlmpIcons.Play,
|
||||
onClick = { AppContainer.player.toggle() },
|
||||
size = 80.dp,
|
||||
iconSize = 26.dp,
|
||||
@@ -393,6 +433,7 @@ fun FullPlayerScreen(
|
||||
}
|
||||
Spacer(Modifier.height(28.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddToMenu) {
|
||||
@@ -426,6 +467,269 @@ fun FullPlayerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LandscapePlayerBody(
|
||||
current: Music?,
|
||||
isPlaying: Boolean,
|
||||
loopMode: LoopMode,
|
||||
lyric: LoadedLyric,
|
||||
lrcType: Int,
|
||||
isLove: Boolean,
|
||||
dark: Boolean,
|
||||
iconTint: Color,
|
||||
controlBg: Color,
|
||||
thumbColor: Color,
|
||||
activeTrack: Color,
|
||||
inactiveTrack: Color,
|
||||
timeColor: Color,
|
||||
lyricPlayingColor: Color,
|
||||
lyricOtherColor: Color,
|
||||
listState: LazyListState,
|
||||
onBack: () -> Unit,
|
||||
onMore: () -> Unit,
|
||||
onToggleLove: () -> Unit,
|
||||
onAddSong: () -> Unit,
|
||||
onToggleTranslate: () -> Unit,
|
||||
onShowPlaylist: () -> Unit,
|
||||
) {
|
||||
val (safeStart, safeEnd) = unscaledSafeDrawingHorizontal()
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
start = safeStart + 28.dp,
|
||||
end = safeEnd + 28.dp,
|
||||
top = 12.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.weight(0.42f)
|
||||
.fillMaxHeight()
|
||||
.playerCollapseDrag()
|
||||
.padding(end = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(40.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NeumorphicIconButton(
|
||||
imageVector = Icons.Filled.KeyboardArrowDown,
|
||||
onClick = onBack,
|
||||
size = 32.dp,
|
||||
iconSize = 20.dp,
|
||||
radius = 6.dp,
|
||||
contentPadding = 5.dp,
|
||||
iconColor = iconTint,
|
||||
background = Color.Transparent,
|
||||
contentDescription = tr("collapse"),
|
||||
)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
val side = maxWidth.coerceAtMost(maxHeight).coerceAtLeast(1.dp)
|
||||
PlayerCoverArt(music = current, size = side)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = current?.musicName ?: tr("not_playing"),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TextStyle(
|
||||
color = if (dark) ColorMs.White else ColorMs.Text333,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
)
|
||||
val artist = current?.artist.orEmpty()
|
||||
if (artist.isNotBlank()) {
|
||||
Text(
|
||||
text = artist,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TextStyle(
|
||||
color = if (dark) ColorMs.GrayDFD else ColorMs.Gray999,
|
||||
fontSize = 12.sp,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
PlayerFuncButtons(
|
||||
content = PlayerContent.Cover,
|
||||
isLove = isLove,
|
||||
lrcType = lrcType,
|
||||
dark = dark,
|
||||
iconTint = iconTint,
|
||||
onToggleLove = onToggleLove,
|
||||
onAddSong = onAddSong,
|
||||
onToggleTranslate = onToggleTranslate,
|
||||
)
|
||||
PlayerSeekBar(
|
||||
thumbColor = thumbColor,
|
||||
activeTrack = activeTrack,
|
||||
inactiveTrack = inactiveTrack,
|
||||
timeColor = timeColor,
|
||||
onSeek = { AppContainer.player.seekTo(it) },
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NeumorphicIconButton(
|
||||
icon = when (loopMode) {
|
||||
LoopMode.LIST -> LlmpIcons.Loop
|
||||
LoopMode.SINGLE -> LlmpIcons.LoopOne
|
||||
LoopMode.SHUFFLE -> LlmpIcons.Shuffle
|
||||
},
|
||||
onClick = {
|
||||
val next = when (loopMode) {
|
||||
LoopMode.LIST -> LoopMode.SINGLE
|
||||
LoopMode.SINGLE -> LoopMode.SHUFFLE
|
||||
LoopMode.SHUFFLE -> LoopMode.LIST
|
||||
}
|
||||
AppContainer.player.setLoopMode(next)
|
||||
AppContainer.settings.loopMode = next
|
||||
},
|
||||
size = 28.dp,
|
||||
iconSize = 14.dp,
|
||||
contentPadding = 7.dp,
|
||||
iconColor = iconTint,
|
||||
background = controlBg,
|
||||
)
|
||||
NeumorphicIconButton(
|
||||
icon = LlmpIcons.Prev,
|
||||
onClick = { AppContainer.player.previous() },
|
||||
size = 44.dp,
|
||||
iconSize = 14.dp,
|
||||
radius = 22.dp,
|
||||
contentPadding = 14.dp,
|
||||
iconColor = iconTint,
|
||||
background = controlBg,
|
||||
)
|
||||
NeumorphicIconButton(
|
||||
icon = if (isPlaying && current != null) LlmpIcons.Pause else LlmpIcons.Play,
|
||||
onClick = { AppContainer.player.toggle() },
|
||||
size = 56.dp,
|
||||
iconSize = 20.dp,
|
||||
radius = 28.dp,
|
||||
contentPadding = 16.dp,
|
||||
iconColor = iconTint,
|
||||
background = controlBg,
|
||||
)
|
||||
NeumorphicIconButton(
|
||||
icon = LlmpIcons.Next,
|
||||
onClick = { AppContainer.player.next() },
|
||||
size = 44.dp,
|
||||
iconSize = 14.dp,
|
||||
radius = 22.dp,
|
||||
contentPadding = 14.dp,
|
||||
iconColor = iconTint,
|
||||
background = controlBg,
|
||||
)
|
||||
NeumorphicIconButton(
|
||||
icon = LlmpIcons.Playlist,
|
||||
onClick = onShowPlaylist,
|
||||
size = 28.dp,
|
||||
iconSize = 14.dp,
|
||||
contentPadding = 7.dp,
|
||||
iconColor = iconTint,
|
||||
background = controlBg,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.weight(0.58f)
|
||||
.fillMaxHeight()
|
||||
.padding(start = 8.dp),
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(40.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (PlatformInfo.allowEulaLyric) {
|
||||
PlayerFuncButtons(
|
||||
content = PlayerContent.Lyric,
|
||||
isLove = isLove,
|
||||
lrcType = lrcType,
|
||||
dark = dark,
|
||||
iconTint = iconTint,
|
||||
onToggleLove = onToggleLove,
|
||||
onAddSong = onAddSong,
|
||||
onToggleTranslate = onToggleTranslate,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
NeumorphicIconButton(
|
||||
imageVector = Icons.Filled.MoreHoriz,
|
||||
onClick = onMore,
|
||||
size = 32.dp,
|
||||
iconSize = 18.dp,
|
||||
radius = 6.dp,
|
||||
contentPadding = 5.dp,
|
||||
iconColor = iconTint,
|
||||
background = Color.Transparent,
|
||||
contentDescription = tr("more"),
|
||||
)
|
||||
}
|
||||
PlayerLyricScroll(
|
||||
lyric = lyric,
|
||||
listState = listState,
|
||||
playingColor = lyricPlayingColor,
|
||||
otherColor = lyricOtherColor,
|
||||
onTapBack = {},
|
||||
onSeek = { positionMs ->
|
||||
AppContainer.player.seekTo(positionMs)
|
||||
AppContainer.player.play()
|
||||
},
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.playerCollapseDrag(): Modifier = pointerInput(Unit) {
|
||||
var lastDrag = 0f
|
||||
detectVerticalDragGestures(
|
||||
onDragEnd = {
|
||||
AppContainer.weSlide.onChromeVerticalDragEnd?.invoke(lastDrag)
|
||||
lastDrag = 0f
|
||||
},
|
||||
onDragCancel = {
|
||||
AppContainer.weSlide.onChromeVerticalDragCancel?.invoke()
|
||||
lastDrag = 0f
|
||||
},
|
||||
onVerticalDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
lastDrag = dragAmount
|
||||
AppContainer.weSlide.onChromeVerticalDrag?.invoke(dragAmount)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlayerHeader(
|
||||
title: String,
|
||||
@@ -599,10 +903,11 @@ private fun PlayerLyricScroll(
|
||||
otherColor: Color,
|
||||
onTapBack: () -> Unit,
|
||||
onSeek: (positionMs: Long) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (lyric.main.isEmpty()) {
|
||||
Box(
|
||||
Modifier
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(onTapBack) {
|
||||
detectTapGestures(onTap = { onTapBack() })
|
||||
@@ -675,7 +980,7 @@ private fun PlayerLyricScroll(
|
||||
selectedIndex = -1
|
||||
}
|
||||
|
||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
BoxWithConstraints(modifier.fillMaxSize()) {
|
||||
// 半视口留白,使首尾行可居中到锚点。
|
||||
val edgePad = maxHeight / 2
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
@@ -865,11 +1170,12 @@ private fun PlayerFuncButtons(
|
||||
onToggleLove: () -> Unit,
|
||||
onAddSong: () -> Unit,
|
||||
onToggleTranslate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val btnBg = Color.Transparent
|
||||
val btnIcon = iconTint
|
||||
Row(
|
||||
Modifier
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -944,13 +1250,13 @@ private fun PlayerFuncButtons(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlayerCoverArt(music: Music?) {
|
||||
private fun PlayerCoverArt(music: Music?, size: Dp = 273.dp) {
|
||||
val dark = LocalDarkTheme.current
|
||||
val coverUri = rememberMusicCoverUri(music)
|
||||
val hasCover = !coverUri.isNullOrBlank()
|
||||
Box(
|
||||
Modifier
|
||||
.size(273.dp)
|
||||
.size(size)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(if (dark) ColorMs.Dark2B333A else ColorMs.E7F2FF),
|
||||
contentAlignment = Alignment.Center,
|
||||
|
||||
+34
-18
@@ -11,6 +11,7 @@ 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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -73,6 +74,8 @@ fun MiniPlayerBar(
|
||||
modifier: Modifier = Modifier,
|
||||
/** 仅选择模式为 0 时可点击封面打开全屏播放器。 */
|
||||
interactionEnabled: Boolean = true,
|
||||
/** 嵌进底栏右侧时去掉悬浮胶囊,铺满父级高度。 */
|
||||
embedded: Boolean = false,
|
||||
) {
|
||||
val current by remember {
|
||||
AppContainer.player.state.map { it.current }.distinctUntilChanged()
|
||||
@@ -93,27 +96,40 @@ fun MiniPlayerBar(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, top = 6.dp),
|
||||
.then(
|
||||
if (embedded) {
|
||||
Modifier.fillMaxHeight()
|
||||
} else {
|
||||
Modifier.padding(start = 16.dp, end = 16.dp, top = 6.dp)
|
||||
},
|
||||
),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)
|
||||
.clip(RoundedCornerShape(34.dp))
|
||||
.background(barColor)
|
||||
.then(
|
||||
if (hazeState != null) {
|
||||
Modifier.hazeEffect(state = hazeState) {
|
||||
blurRadius = 30.dp
|
||||
backgroundColor = barColor
|
||||
tints = listOf(
|
||||
HazeTint(barColor),
|
||||
HazeTint(Color.Gray.copy(alpha = 0.15f)),
|
||||
)
|
||||
noiseFactor = 0f
|
||||
}
|
||||
if (embedded) {
|
||||
Modifier.fillMaxHeight()
|
||||
} else {
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
.clip(RoundedCornerShape(34.dp))
|
||||
.background(barColor)
|
||||
.then(
|
||||
if (hazeState != null) {
|
||||
Modifier.hazeEffect(state = hazeState) {
|
||||
blurRadius = 30.dp
|
||||
backgroundColor = barColor
|
||||
tints = listOf(
|
||||
HazeTint(barColor),
|
||||
HazeTint(Color.Gray.copy(alpha = 0.15f)),
|
||||
)
|
||||
noiseFactor = 0f
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
) {
|
||||
@@ -142,7 +158,7 @@ fun MiniPlayerBar(
|
||||
PlayerMusicCover(
|
||||
music = current,
|
||||
circular = true,
|
||||
fallbackPadding = 8.dp,
|
||||
fallbackPadding = if (embedded) 2.dp else 8.dp,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -185,10 +201,10 @@ fun MiniPlayerBar(
|
||||
Spacer(Modifier.width(8.dp))
|
||||
// 新拟态按钮 30×30,图标 15,内边距 5,无阴影
|
||||
MiniBarIconButton(
|
||||
asset = if (isPlaying) LlmpIcons.Pause else LlmpIcons.Play,
|
||||
asset = if (isPlaying && current != null) LlmpIcons.Pause else LlmpIcons.Play,
|
||||
tint = iconColor,
|
||||
contentDescription = if (isPlaying) tr("pause") else tr("play"),
|
||||
onClick = if (interactionEnabled) {
|
||||
contentDescription = if (isPlaying && current != null) tr("pause") else tr("play"),
|
||||
onClick = if (interactionEnabled && current != null) {
|
||||
{ AppContainer.player.toggle() }
|
||||
} else {
|
||||
null
|
||||
|
||||
+271
-2
@@ -84,6 +84,7 @@ import com.zhushenwudi.lovelivemusicplayer.lyric.LoadedLyric
|
||||
import com.zhushenwudi.lovelivemusicplayer.lyric.LrcParser
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.DetailBackHandler
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.PlatformUi
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.Music
|
||||
import com.zhushenwudi.lovelivemusicplayer.player.LoopMode
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.LlmpAsyncImage
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.clearLlmpImageCache
|
||||
@@ -109,6 +110,7 @@ import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
||||
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.LocalIsLandscape
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.hSp
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -487,9 +489,15 @@ fun TransferScreen(onBack: () -> Unit, onOpenScan: () -> Unit) {
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(bg),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppHeader(title = tr("music_trans"), onBack = { requestBack() })
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (connected || state.completed) {
|
||||
Spacer(Modifier.height(35.dp))
|
||||
TransferCoverBlock(
|
||||
@@ -552,6 +560,7 @@ fun TransferScreen(onBack: () -> Unit, onOpenScan: () -> Unit) {
|
||||
Text(it, color = ColorMs.PinkF940A7, fontSize = hSp(12f))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showLeaveConfirm) {
|
||||
TwoButtonDialog(
|
||||
@@ -888,6 +897,23 @@ fun DriveModeScreen(onBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
if (LocalIsLandscape.current) {
|
||||
DriveModeLandscape(
|
||||
onBack = onBack,
|
||||
queue = state.queue,
|
||||
index = state.index,
|
||||
isPlaying = state.isPlaying,
|
||||
loopMode = state.loopMode,
|
||||
dark = dark,
|
||||
contentBg = contentBg,
|
||||
line = line,
|
||||
musicName = music?.musicName,
|
||||
musicId = music?.musicId,
|
||||
isLove = isLove,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 外层背景 + 内容区背景 + 底部栏外层背景
|
||||
Column(
|
||||
Modifier
|
||||
@@ -993,7 +1019,7 @@ fun DriveModeScreen(onBack: () -> Unit) {
|
||||
Spacer(Modifier.width(38.dp))
|
||||
// 播放按钮 90×90,外内边距 8,图标无描边环。
|
||||
NeumorphicIconButton(
|
||||
icon = if (state.isPlaying) {
|
||||
icon = if (state.isPlaying && state.current != null) {
|
||||
SvgAsset("drawable/drive_car_play_button.svg")
|
||||
} else {
|
||||
SvgAsset("drawable/drive_car_pause_button.svg")
|
||||
@@ -1089,6 +1115,249 @@ fun DriveModeScreen(onBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DriveModeLandscape(
|
||||
onBack: () -> Unit,
|
||||
queue: List<Music>,
|
||||
index: Int,
|
||||
isPlaying: Boolean,
|
||||
loopMode: LoopMode,
|
||||
dark: Boolean,
|
||||
contentBg: Color,
|
||||
line: String,
|
||||
musicName: String?,
|
||||
musicId: String?,
|
||||
isLove: Boolean,
|
||||
) {
|
||||
val iconTint = if (dark) ColorMs.White else Color.Black
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(contentBg)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 28.dp, vertical = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
DriveLandscapeAction(
|
||||
text = tr("exit"),
|
||||
icon = SvgAsset("drawable/drive_car_exit.svg"),
|
||||
color = ColorMs.DriveExit,
|
||||
onClick = onBack,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1.15f)
|
||||
.fillMaxHeight(),
|
||||
) {
|
||||
key(queue.size, queue.firstOrNull()?.musicId, queue.lastOrNull()?.musicId) {
|
||||
DriveModeCarousel(
|
||||
queue = queue,
|
||||
currentIndex = index,
|
||||
onPageSettled = { page ->
|
||||
if (page != index) AppContainer.player.playAtIndex(page)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(start = 28.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
musicName ?: tr("no_songs"),
|
||||
color = if (dark) ColorMs.White else ColorMs.Text333,
|
||||
fontSize = hSp(26f),
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
line,
|
||||
color = if (dark) ColorMs.White else ColorMs.Text333,
|
||||
fontSize = hSp(18f),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(28.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
NeumorphicIconButton(
|
||||
icon = LlmpIcons.Prev,
|
||||
onClick = { AppContainer.player.previous() },
|
||||
size = 64.dp,
|
||||
iconSize = 22.dp,
|
||||
radius = 32.dp,
|
||||
background = contentBg,
|
||||
iconColor = iconTint,
|
||||
hasShadow = true,
|
||||
contentPadding = 18.dp,
|
||||
)
|
||||
Spacer(Modifier.width(28.dp))
|
||||
NeumorphicIconButton(
|
||||
icon = if (isPlaying && musicId != null) {
|
||||
SvgAsset("drawable/drive_car_play_button.svg")
|
||||
} else {
|
||||
SvgAsset("drawable/drive_car_pause_button.svg")
|
||||
},
|
||||
onClick = { AppContainer.player.toggle() },
|
||||
size = 96.dp,
|
||||
iconSize = 42.dp,
|
||||
radius = 48.dp,
|
||||
background = contentBg,
|
||||
iconColor = iconTint,
|
||||
hasShadow = true,
|
||||
contentPadding = 24.dp,
|
||||
)
|
||||
Spacer(Modifier.width(28.dp))
|
||||
NeumorphicIconButton(
|
||||
icon = LlmpIcons.Next,
|
||||
onClick = { AppContainer.player.next() },
|
||||
size = 64.dp,
|
||||
iconSize = 22.dp,
|
||||
radius = 32.dp,
|
||||
background = contentBg,
|
||||
iconColor = iconTint,
|
||||
hasShadow = true,
|
||||
contentPadding = 18.dp,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(28.dp))
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
NeumorphicIconButton(
|
||||
icon = if (isLove) {
|
||||
SvgAsset("drawable/drive_car_favoriate.svg")
|
||||
} else {
|
||||
SvgAsset("drawable/drive_car_dis_favorite.svg")
|
||||
},
|
||||
onClick = { musicId?.let { AppContainer.toggleLove(it) } },
|
||||
size = 52.dp,
|
||||
iconSize = 28.dp,
|
||||
radius = 26.dp,
|
||||
background = contentBg,
|
||||
iconColor = if (isLove) ColorMs.PinkF940A7 else iconTint,
|
||||
hasShadow = true,
|
||||
contentPadding = 10.dp,
|
||||
)
|
||||
NeumorphicIconButton(
|
||||
icon = when (loopMode) {
|
||||
LoopMode.LIST -> LlmpIcons.Loop
|
||||
LoopMode.SINGLE -> LlmpIcons.LoopOne
|
||||
LoopMode.SHUFFLE -> LlmpIcons.Shuffle
|
||||
},
|
||||
onClick = {
|
||||
val next = when (loopMode) {
|
||||
LoopMode.LIST -> LoopMode.SINGLE
|
||||
LoopMode.SINGLE -> LoopMode.SHUFFLE
|
||||
LoopMode.SHUFFLE -> LoopMode.LIST
|
||||
}
|
||||
AppContainer.settings.loopMode = next
|
||||
AppContainer.player.setLoopMode(next)
|
||||
},
|
||||
size = 52.dp,
|
||||
iconSize = 24.dp,
|
||||
radius = 26.dp,
|
||||
background = contentBg,
|
||||
iconColor = iconTint,
|
||||
hasShadow = true,
|
||||
contentPadding = 12.dp,
|
||||
)
|
||||
DriveLandscapeAction(
|
||||
text = tr("iLove"),
|
||||
icon = SvgAsset("drawable/drive_car_favorite_bottom.svg"),
|
||||
color = ColorMs.DriveFavorite,
|
||||
onClick = {
|
||||
val loved = AppContainer.library.getAllLove()
|
||||
.mapNotNull { AppContainer.library.getMusicById(it.musicId) }
|
||||
if (loved.isNotEmpty()) {
|
||||
AppContainer.player.setQueue(loved, 0)
|
||||
AppContainer.player.play()
|
||||
AppContainer.persistPlayQueue()
|
||||
}
|
||||
},
|
||||
)
|
||||
DriveLandscapeAction(
|
||||
text = tr("history_short"),
|
||||
icon = SvgAsset("drawable/drive_car_playlist_bottom.svg"),
|
||||
color = ColorMs.DriveHistory,
|
||||
onClick = {
|
||||
val recent = AppContainer.library.getHistoryMusic()
|
||||
if (recent.isNotEmpty()) {
|
||||
AppContainer.player.setQueue(recent, 0)
|
||||
AppContainer.player.play()
|
||||
AppContainer.persistPlayQueue()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DriveLandscapeAction(
|
||||
text: String,
|
||||
icon: SvgAsset,
|
||||
color: Color,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier
|
||||
.height(52.dp)
|
||||
.clip(RoundedCornerShape(26.dp))
|
||||
.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(horizontal = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NeumorphicIconButton(
|
||||
icon = icon,
|
||||
onClick = null,
|
||||
size = 28.dp,
|
||||
iconSize = 28.dp,
|
||||
iconColor = color,
|
||||
hasShadow = false,
|
||||
contentPadding = 0.dp,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(text, color = color, fontSize = hSp(16f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DriveBottomItem(
|
||||
text: String,
|
||||
|
||||
+17
-8
@@ -71,6 +71,7 @@ import com.zhushenwudi.lovelivemusicplayer.ui.components.ArtistListItem
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.ArtistPhotoImage
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.CoverPlaceholder
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.DetailsPage
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.LocalDetailCoverSize
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.DialogMoreWithMusic
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.DialogSongInfo
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.ListItemSubtitleLineHeight
|
||||
@@ -91,6 +92,7 @@ import com.zhushenwudi.lovelivemusicplayer.ui.components.TwoLineListTextColumn
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.addMusicPlayNext
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalHomeListBottomInset
|
||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||
|
||||
@Composable
|
||||
fun LibraryTabContent(
|
||||
@@ -261,7 +263,11 @@ private fun AlbumList(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(3),
|
||||
columns = if (LocalIsLandscape.current) {
|
||||
GridCells.Adaptive(108.dp)
|
||||
} else {
|
||||
GridCells.Fixed(3)
|
||||
},
|
||||
state = resolvedGridState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
@@ -502,6 +508,7 @@ fun AlbumDetailScreen(albumId: String, onBack: () -> Unit, onOpenPlayer: () -> U
|
||||
onBack = onBack,
|
||||
onOpenPlayer = onOpenPlayer,
|
||||
cover = {
|
||||
val coverSize = LocalDetailCoverSize.current
|
||||
AlbumDetailsCover(
|
||||
albumName = album?.albumName.orEmpty(),
|
||||
category = album?.category,
|
||||
@@ -510,9 +517,9 @@ fun AlbumDetailScreen(albumId: String, onBack: () -> Unit, onOpenPlayer: () -> U
|
||||
if (album != null) {
|
||||
AlbumCoverImage(
|
||||
album = album,
|
||||
// 图片高度未指定时回退为宽度(240)。
|
||||
// 图片高度未指定时回退为宽度。竖屏 240,横屏随左侧栏缩小。
|
||||
modifier = Modifier
|
||||
.size(240.dp)
|
||||
.size(coverSize)
|
||||
.llmpDetailSharedElement(
|
||||
HeroKeys.album(albumId),
|
||||
HeroRoundedClip,
|
||||
@@ -522,7 +529,7 @@ fun AlbumDetailScreen(albumId: String, onBack: () -> Unit, onOpenPlayer: () -> U
|
||||
} else {
|
||||
CoverPlaceholder(
|
||||
label = tr("album_abbr"),
|
||||
size = 240.dp,
|
||||
size = coverSize,
|
||||
radius = 12.dp,
|
||||
modifier = Modifier.llmpDetailSharedElement(
|
||||
HeroKeys.album(albumId),
|
||||
@@ -551,10 +558,11 @@ fun ArtistDetailScreen(artistId: String, onBack: () -> Unit, onOpenPlayer: () ->
|
||||
onBack = onBack,
|
||||
onOpenPlayer = onOpenPlayer,
|
||||
cover = {
|
||||
val coverSize = LocalDetailCoverSize.current
|
||||
if (artist != null) {
|
||||
ArtistPhotoImage(
|
||||
artist = artist,
|
||||
size = 240.dp,
|
||||
size = coverSize,
|
||||
modifier = Modifier.llmpDetailSharedElement(
|
||||
HeroKeys.singer(artistId),
|
||||
HeroCircleClip,
|
||||
@@ -563,7 +571,7 @@ fun ArtistDetailScreen(artistId: String, onBack: () -> Unit, onOpenPlayer: () ->
|
||||
} else {
|
||||
CoverPlaceholder(
|
||||
label = tr("music_abbr"),
|
||||
size = 240.dp,
|
||||
size = coverSize,
|
||||
radius = 120.dp,
|
||||
modifier = Modifier.llmpDetailSharedElement(
|
||||
HeroKeys.singer(artistId),
|
||||
@@ -603,10 +611,11 @@ fun MenuDetailScreen(menuId: Long, onBack: () -> Unit, onOpenPlayer: () -> Unit)
|
||||
songs = AppContainer.library.getMusicByIdsPreservingOrder(nextIds, enableHttp)
|
||||
},
|
||||
cover = {
|
||||
val coverSize = LocalDetailCoverSize.current
|
||||
if (coverMusic != null) {
|
||||
MusicCoverImage(
|
||||
music = coverMusic,
|
||||
size = 240.dp,
|
||||
size = coverSize,
|
||||
radius = 12.dp,
|
||||
modifier = Modifier.llmpDetailSharedElement(
|
||||
HeroKeys.menu(menuId),
|
||||
@@ -616,7 +625,7 @@ fun MenuDetailScreen(menuId: Long, onBack: () -> Unit, onOpenPlayer: () -> Unit)
|
||||
} else {
|
||||
CoverPlaceholder(
|
||||
label = menu?.name?.take(1).orEmpty().ifEmpty { tr("menu_abbr") },
|
||||
size = 240.dp,
|
||||
size = coverSize,
|
||||
radius = 12.dp,
|
||||
modifier = Modifier.llmpDetailSharedElement(
|
||||
HeroKeys.menu(menuId),
|
||||
|
||||
+7
-3
@@ -29,6 +29,8 @@ 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 com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
@@ -73,18 +75,20 @@ fun SplashScreen(onFinished: () -> Unit) {
|
||||
.fillMaxSize()
|
||||
.background(Color.White),
|
||||
) {
|
||||
val landscape = LocalIsLandscape.current
|
||||
val model = photoPath ?: Res.getUri("drawable/launch_background.png")
|
||||
LlmpAsyncImage(
|
||||
model = model,
|
||||
contentDescription = null,
|
||||
contentScale = if (photoPath != null) ContentScale.Crop else ContentScale.Fit,
|
||||
alignment = Alignment.TopCenter,
|
||||
contentScale = if (photoPath != null && !landscape) ContentScale.Crop else ContentScale.Fit,
|
||||
alignment = if (landscape) Alignment.Center else Alignment.TopCenter,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 25.dp, bottom = 50.dp)
|
||||
.then(if (landscape) Modifier.navigationBarsPadding() else Modifier)
|
||||
.padding(end = 25.dp, bottom = if (landscape) 16.dp else 50.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(ColorMs.PinkF940A7.copy(alpha = 100f / 255f))
|
||||
.clickable(
|
||||
|
||||
+21
@@ -4,6 +4,7 @@ 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.safeDrawing
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -17,6 +18,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -28,6 +30,9 @@ import com.zhushenwudi.lovelivemusicplayer.Const
|
||||
*/
|
||||
val LocalDesignScale = staticCompositionLocalOf { 1f }
|
||||
|
||||
/** 窗口宽大于高。竖屏组合树不读这个值。 */
|
||||
val LocalIsLandscape = staticCompositionLocalOf { false }
|
||||
|
||||
/** [com.zhushenwudi.lovelivemusicplayer.ui.theme.ProvideDesignDensity] 缩放前的设备密度(供底部栏使用)。 */
|
||||
val LocalUnscaledDensity = staticCompositionLocalOf<Density?> { null }
|
||||
|
||||
@@ -62,7 +67,10 @@ fun ProvideDesignDensity(content: @Composable () -> Unit) {
|
||||
return@CompositionLocalProvider
|
||||
}
|
||||
val heightDp = heightPx / base.density
|
||||
val widthPx = containerPx.width
|
||||
val isLandscape = widthPx > 0 && widthPx > heightPx
|
||||
// 按高度比例缩放;不低于设计尺寸;平板上限 1.35。忽略系统字体缩放(对齐 Flutter textScaler = 1.0)。
|
||||
// 横屏高度变矮时下限仍是 1,不会按变矮的高度放大或缩小。
|
||||
val scale = (heightDp / Const.UI_HEIGHT).coerceIn(1f, 1.35f)
|
||||
val scaled = remember(base.density, scale) {
|
||||
Density(
|
||||
@@ -74,6 +82,7 @@ fun ProvideDesignDensity(content: @Composable () -> Unit) {
|
||||
LocalDensity provides scaled,
|
||||
LocalUnscaledDensity provides base,
|
||||
LocalDesignScale provides scale,
|
||||
LocalIsLandscape provides isLandscape,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
@@ -104,6 +113,18 @@ fun unscaledStatusBarsTop(): Dp = unscaledSystemBarPx(
|
||||
startupPx = LocalStartupWindowMetrics.current.statusTopPx,
|
||||
)
|
||||
|
||||
/**
|
||||
* 左右安全区(刘海、手势条),dp 使用真实设备密度,再换算到当前缩放密度。
|
||||
*/
|
||||
@Composable
|
||||
fun unscaledSafeDrawingHorizontal(): Pair<Dp, Dp> {
|
||||
val density = systemBarDensity()
|
||||
val insets = WindowInsets.safeDrawing
|
||||
val start = with(density) { insets.getLeft(this, LayoutDirection.Ltr).toDp() }
|
||||
val end = with(density) { insets.getRight(this, LayoutDirection.Ltr).toDp() }
|
||||
return start.asUnscaledInScaledContext() to end.asUnscaledInScaledContext()
|
||||
}
|
||||
|
||||
/** 导航栏底部内边距(dp,使用真实设备密度,忽略设计缩放)。 */
|
||||
@Composable
|
||||
fun unscaledNavigationBarsBottom(): Dp = unscaledSystemBarPx(
|
||||
|
||||
Reference in New Issue
Block a user