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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
@@ -42,6 +44,13 @@ import androidx.compose.runtime.snapshotFlow
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
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.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
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.AddToMenuDialog
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.AppDrawer
|
import com.zhushenwudi.lovelivemusicplayer.ui.components.AppDrawer
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.BatchActionBar
|
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.LibrarySelectMode
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.LlmpBottomBar
|
import com.zhushenwudi.lovelivemusicplayer.ui.components.LlmpBottomBar
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.LoadingOverlay
|
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.scroll.rememberStatusBarScrollToTopAction
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.HomeListChrome
|
import com.zhushenwudi.lovelivemusicplayer.ui.theme.HomeListChrome
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalHomeListBottomInset
|
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.ProvideUnscaledDensity
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.asUnscaledInScaledContext
|
import com.zhushenwudi.lovelivemusicplayer.ui.theme.asUnscaledInScaledContext
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.unscaledNavigationBarsBottom
|
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.theme.unscaledStatusBarsTop
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.weslide.WeSlide
|
import com.zhushenwudi.lovelivemusicplayer.ui.weslide.WeSlide
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
@@ -156,8 +168,10 @@ fun MainShell(
|
|||||||
// 选择模式 > 0 时锁定歌库/我的标签与 WeSlide 手势
|
// 选择模式 > 0 时锁定歌库/我的标签与 WeSlide 手势
|
||||||
val batchLock = selectMode == LibrarySelectMode.Batch
|
val batchLock = selectMode == LibrarySelectMode.Batch
|
||||||
val detailOpen = libraryDetail != null
|
val detailOpen = libraryDetail != null
|
||||||
LaunchedEffect(weSlide.isOpened, detailOpen) {
|
val isLandscape = LocalIsLandscape.current
|
||||||
if (weSlide.isOpened || detailOpen) {
|
LaunchedEffect(weSlide.isOpened, detailOpen, isLandscape) {
|
||||||
|
// 横屏详情仍保留底栏(三个入口 + 迷你条);全屏播放器展开时收起。
|
||||||
|
if (weSlide.isOpened || (detailOpen && !isLandscape)) {
|
||||||
weSlideFooter.hide()
|
weSlideFooter.hide()
|
||||||
} else {
|
} else {
|
||||||
weSlideFooter.show()
|
weSlideFooter.show()
|
||||||
@@ -500,6 +514,14 @@ fun MainShell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val shellBg = if (dark) ColorMs.NightPrimary else ColorMs.LightPrimary
|
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 footerNavInset = unscaledNavigationBarsBottom()
|
||||||
val footerHeightUnscaled = when {
|
val footerHeightUnscaled = when {
|
||||||
selectMode == LibrarySelectMode.Batch && batchSelectable -> 102.dp + footerNavInset
|
selectMode == LibrarySelectMode.Batch && batchSelectable -> 102.dp + footerNavInset
|
||||||
@@ -507,7 +529,12 @@ fun MainShell(
|
|||||||
}
|
}
|
||||||
val miniPlayerHeight = HomeListChrome.MiniPlayerHeight
|
val miniPlayerHeight = HomeListChrome.MiniPlayerHeight
|
||||||
val footerHeight = footerHeightUnscaled.asUnscaledInScaledContext()
|
val footerHeight = footerHeightUnscaled.asUnscaledInScaledContext()
|
||||||
val panelMinSize = miniPlayerHeight + HomeListChrome.MiniPlayerFooterGap + footerHeight
|
// 横屏迷你条在底栏右侧,收起时面板只露出与底栏等高的一条,由底栏盖住。
|
||||||
|
val panelMinSize = if (isLandscape) {
|
||||||
|
footerHeight
|
||||||
|
} else {
|
||||||
|
miniPlayerHeight + HomeListChrome.MiniPlayerFooterGap + footerHeight
|
||||||
|
}
|
||||||
val statusBarTop = unscaledStatusBarsTop().asUnscaledInScaledContext()
|
val statusBarTop = unscaledStatusBarsTop().asUnscaledInScaledContext()
|
||||||
|
|
||||||
fun importData(force: Boolean) {
|
fun importData(force: Boolean) {
|
||||||
@@ -584,8 +611,13 @@ fun MainShell(
|
|||||||
drawerContent = {
|
drawerContent = {
|
||||||
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
BoxWithConstraints(Modifier.fillMaxHeight()) {
|
BoxWithConstraints(Modifier.fillMaxHeight()) {
|
||||||
// 与原项目 endDrawer:min(0.35 * 屏高, 屏宽),不用 Material 默认 240–360dp。
|
// 竖屏与原项目 endDrawer:min(0.35 * 屏高, 屏宽)。
|
||||||
val drawerWidth = min(maxHeight * 0.35f, maxWidth)
|
// 横屏屏高变矮,改用约 300dp,且不超过屏宽的 45%。
|
||||||
|
val drawerWidth = if (isLandscape) {
|
||||||
|
min(300.dp, maxWidth * 0.45f)
|
||||||
|
} else {
|
||||||
|
min(maxHeight * 0.35f, maxWidth)
|
||||||
|
}
|
||||||
Surface(
|
Surface(
|
||||||
color = if (dark) ColorMs.NightPrimary else ColorMs.White,
|
color = if (dark) ColorMs.NightPrimary else ColorMs.White,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -618,9 +650,42 @@ fun MainShell(
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalLayoutDirection provides LayoutDirection.Ltr,
|
LocalLayoutDirection provides LayoutDirection.Ltr,
|
||||||
LocalMiniPlayerHaze provides if (homeHazeEnabled) miniPlayerHaze else null,
|
LocalMiniPlayerHaze provides if (homeHazeEnabled) miniPlayerHaze else null,
|
||||||
LocalHomeListBottomInset provides HomeListChrome.ListBottomInset,
|
LocalHomeListBottomInset provides if (isLandscape) {
|
||||||
|
16.dp
|
||||||
|
} else {
|
||||||
|
HomeListChrome.ListBottomInset
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
WeSlide(
|
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,
|
controller = weSlide,
|
||||||
footerController = weSlideFooter,
|
footerController = weSlideFooter,
|
||||||
panelMinSize = panelMinSize,
|
panelMinSize = panelMinSize,
|
||||||
@@ -664,8 +729,49 @@ fun MainShell(
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(shellBg)
|
.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(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -704,6 +810,7 @@ fun MainShell(
|
|||||||
hasShadow = true,
|
hasShadow = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (libraryTab != LibraryTab.ARTISTS &&
|
if (libraryTab != LibraryTab.ARTISTS &&
|
||||||
libraryTab != LibraryTab.MENUS
|
libraryTab != LibraryTab.MENUS
|
||||||
@@ -711,7 +818,15 @@ fun MainShell(
|
|||||||
SongLibraryTopBar(
|
SongLibraryTopBar(
|
||||||
countLabel = countLabel,
|
countLabel = countLabel,
|
||||||
libraryTab = libraryTab,
|
libraryTab = libraryTab,
|
||||||
selectMode = selectMode,
|
selectMode = if (
|
||||||
|
isLandscape &&
|
||||||
|
selectMode == LibrarySelectMode.Search
|
||||||
|
) {
|
||||||
|
LibrarySelectMode.Normal
|
||||||
|
} else {
|
||||||
|
selectMode
|
||||||
|
},
|
||||||
|
showSearch = !isLandscape,
|
||||||
sortAsc = currentSortAsc,
|
sortAsc = currentSortAsc,
|
||||||
searchQuery = searchDraft,
|
searchQuery = searchDraft,
|
||||||
checkedCount = checkedIds.size,
|
checkedCount = checkedIds.size,
|
||||||
@@ -797,64 +912,16 @@ fun MainShell(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 进入:右滑;退出:页面渐隐 + 封面 Hero(零位移保持转场时长)。
|
// 进入:右滑;退出:页面渐隐 + 封面 Hero(零位移保持转场时长)。
|
||||||
AnimatedVisibility(
|
// 抽到独立函数,避免外层 Row 把 AnimatedVisibility 解析成行内重载。
|
||||||
|
LibraryDetailOverlay(
|
||||||
visible = detailOpen,
|
visible = detailOpen,
|
||||||
enter = slideInHorizontally(
|
detail = retainedDetail,
|
||||||
animationSpec = tween(
|
onClose = ::closeLibraryDetail,
|
||||||
LlmpTransitionMs,
|
onOpenPlayer = {
|
||||||
easing = EaseOutQuad,
|
closeLibraryDetail()
|
||||||
),
|
weSlide.show()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 点击状态栏将当前曲库列表滚回顶部。
|
// 点击状态栏将当前曲库列表滚回顶部。
|
||||||
if (!detailOpen) {
|
if (!detailOpen) {
|
||||||
@@ -870,11 +937,15 @@ fun MainShell(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
panelHeader = {
|
panelHeader = if (isLandscape) {
|
||||||
MiniPlayerBar(
|
null
|
||||||
onOpenPlayer = { weSlide.show() },
|
} else {
|
||||||
interactionEnabled = !batchLock,
|
{
|
||||||
)
|
MiniPlayerBar(
|
||||||
|
onOpenPlayer = { weSlide.show() },
|
||||||
|
interactionEnabled = !batchLock,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
panel = {
|
panel = {
|
||||||
FullPlayerScreen(
|
FullPlayerScreen(
|
||||||
@@ -884,8 +955,8 @@ fun MainShell(
|
|||||||
},
|
},
|
||||||
footer = {
|
footer = {
|
||||||
ProvideUnscaledDensity {
|
ProvideUnscaledDensity {
|
||||||
if (detailOpen) {
|
if (detailOpen && !isLandscape) {
|
||||||
// 详情页不画底栏,只保留槽位高度动画给迷你条下移。
|
// 竖屏详情不画底栏,只保留槽位高度动画给迷你条下移。
|
||||||
} else if (selectMode == LibrarySelectMode.Batch && batchSelectable) {
|
} else if (selectMode == LibrarySelectMode.Batch && batchSelectable) {
|
||||||
BatchActionBar(
|
BatchActionBar(
|
||||||
showUnlove = libraryTab == LibraryTab.LOVE,
|
showUnlove = libraryTab == LibraryTab.LOVE,
|
||||||
@@ -897,6 +968,54 @@ fun MainShell(
|
|||||||
if (selectedMusic().isNotEmpty()) showUnloveConfirm = true
|
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 {
|
} else {
|
||||||
LlmpBottomBar(
|
LlmpBottomBar(
|
||||||
modifier = Modifier.fillMaxSize(),
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -48,6 +49,8 @@ fun LlmpBottomBar(
|
|||||||
onSelect: (Int) -> Unit,
|
onSelect: (Int) -> Unit,
|
||||||
onSectionChange: (TopSection) -> Unit,
|
onSectionChange: (TopSection) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
/** 横屏底栏宽度够,图标和文字左右排列。 */
|
||||||
|
horizontalLabels: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val dark = LocalDarkTheme.current
|
val dark = LocalDarkTheme.current
|
||||||
val tabStripHeight = HomeListChrome.BottomBarContentHeight
|
val tabStripHeight = HomeListChrome.BottomBarContentHeight
|
||||||
@@ -99,38 +102,62 @@ fun LlmpBottomBar(
|
|||||||
) {
|
) {
|
||||||
tabs.forEachIndexed { index, tab ->
|
tabs.forEachIndexed { index, tab ->
|
||||||
val selected = pageSection == section && index == selectedIndex
|
val selected = pageSection == section && index == selectedIndex
|
||||||
Column(
|
val tint = if (selected) ColorMs.PinkF940A7 else ColorMs.UnselectedIcon
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
val labelColor = if (selected) {
|
||||||
verticalArrangement = Arrangement.Center,
|
ColorMs.BottomSelectedText
|
||||||
modifier = Modifier
|
} else {
|
||||||
.weight(1f)
|
ColorMs.TabUnselected.copy(alpha = 0.5f)
|
||||||
.fillMaxHeight()
|
}
|
||||||
.clickable(
|
val itemModifier = Modifier
|
||||||
indication = null,
|
.weight(1f)
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
.fillMaxHeight()
|
||||||
) {
|
.clickable(
|
||||||
if (pageSection != section) {
|
indication = null,
|
||||||
onSectionChange(pageSection)
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
}
|
) {
|
||||||
onSelect(index)
|
if (pageSection != section) {
|
||||||
},
|
onSectionChange(pageSection)
|
||||||
) {
|
}
|
||||||
TintIcon(
|
onSelect(index)
|
||||||
asset = tab.icon,
|
}
|
||||||
tint = if (selected) ColorMs.PinkF940A7 else ColorMs.UnselectedIcon,
|
if (horizontalLabels) {
|
||||||
size = 18.dp,
|
Row(
|
||||||
contentDescription = tr(tab.titleKey),
|
modifier = itemModifier,
|
||||||
)
|
horizontalArrangement = Arrangement.Center,
|
||||||
Spacer(Modifier.height(4.dp))
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
Text(
|
) {
|
||||||
text = tr(tab.titleKey),
|
TintIcon(
|
||||||
fontSize = 10.sp,
|
asset = tab.icon,
|
||||||
color = if (selected) {
|
tint = tint,
|
||||||
ColorMs.BottomSelectedText
|
size = 18.dp,
|
||||||
} else {
|
contentDescription = tr(tab.titleKey),
|
||||||
ColorMs.TabUnselected.copy(alpha = 0.5f)
|
)
|
||||||
},
|
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.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -21,20 +23,25 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.AppContainer
|
||||||
import com.zhushenwudi.lovelivemusicplayer.domain.Music
|
import com.zhushenwudi.lovelivemusicplayer.domain.Music
|
||||||
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
||||||
@@ -49,6 +56,9 @@ import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
|||||||
/** 对齐 Flutter 末项 `95.h`:只挡住迷你播放器,不再叠加已收起的底栏高度。 */
|
/** 对齐 Flutter 末项 `95.h`:只挡住迷你播放器,不再叠加已收起的底栏高度。 */
|
||||||
private val DetailsListBottomInset = 95.dp
|
private val DetailsListBottomInset = 95.dp
|
||||||
|
|
||||||
|
/** 详情封面边长。竖屏 240;横屏按左侧栏剩余高度缩小。 */
|
||||||
|
val LocalDetailCoverSize = staticCompositionLocalOf { 240.dp }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 详情页:顶栏、封面、粘性播放栏、歌曲列表。
|
* 详情页:顶栏、封面、粘性播放栏、歌曲列表。
|
||||||
*/
|
*/
|
||||||
@@ -145,6 +155,38 @@ fun DetailsPage(
|
|||||||
.background(scaffoldBg)
|
.background(scaffoldBg)
|
||||||
.graphicsLayer { alpha = bodyAlpha },
|
.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()) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
AppHeader(title = title, onBack = onBack, modifier = bodyFade)
|
AppHeader(title = title, onBack = onBack, modifier = bodyFade)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
@@ -213,6 +255,7 @@ fun DetailsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (selectMode) {
|
if (selectMode) {
|
||||||
BatchActionBar(
|
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
|
@Composable
|
||||||
fun DetailsListTop(
|
fun DetailsListTop(
|
||||||
|
|||||||
+9
-2
@@ -52,6 +52,7 @@ import com.zhushenwudi.lovelivemusicplayer.i18n.I18n
|
|||||||
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
||||||
import com.zhushenwudi.lovelivemusicplayer.theme.ColorMs
|
import com.zhushenwudi.lovelivemusicplayer.theme.ColorMs
|
||||||
import com.zhushenwudi.lovelivemusicplayer.theme.LocalDarkTheme
|
import com.zhushenwudi.lovelivemusicplayer.theme.LocalDarkTheme
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 双按钮对话框 — 对齐 Flutter [TwoButtonDialog]。
|
* 双按钮对话框 — 对齐 Flutter [TwoButtonDialog]。
|
||||||
@@ -360,8 +361,14 @@ private fun LlmpDialogShell(
|
|||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
// 记住首次布局宽度,避免键盘弹出时 adjustResize 缩小 maxHeight 导致弹窗变窄。
|
// 记住首次布局宽度,避免键盘弹出时 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(
|
Column(
|
||||||
modifier = Modifier
|
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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.domain.GroupKey
|
||||||
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
import com.zhushenwudi.lovelivemusicplayer.i18n.tr
|
||||||
import com.zhushenwudi.lovelivemusicplayer.navigation.LibraryTab
|
import com.zhushenwudi.lovelivemusicplayer.navigation.LibraryTab
|
||||||
import com.zhushenwudi.lovelivemusicplayer.navigation.TopSection
|
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.theme.katongFontFamily
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.SvgAsset
|
import com.zhushenwudi.lovelivemusicplayer.ui.icons.SvgAsset
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.ui.icons.TintIcon
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopLibraryTabs(
|
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")
|
private val FilterIcon = SvgAsset("drawable/main_ic_function.svg")
|
||||||
|
|
||||||
/** 选择模式:0 正常,1 批量,2 搜索。 */
|
/** 选择模式:0 正常,1 批量,2 搜索。 */
|
||||||
@@ -119,6 +254,7 @@ fun SongLibraryTopBar(
|
|||||||
onSelectAll: (Boolean) -> Unit,
|
onSelectAll: (Boolean) -> Unit,
|
||||||
onBatchFinish: () -> Unit,
|
onBatchFinish: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
showSearch: Boolean = true,
|
||||||
) {
|
) {
|
||||||
when (selectMode) {
|
when (selectMode) {
|
||||||
LibrarySelectMode.Batch -> BatchTopBar(
|
LibrarySelectMode.Batch -> BatchTopBar(
|
||||||
@@ -145,6 +281,7 @@ fun SongLibraryTopBar(
|
|||||||
onSearchOpen = onSearchOpen,
|
onSearchOpen = onSearchOpen,
|
||||||
onSort = onSort,
|
onSort = onSort,
|
||||||
onBatchOpen = onBatchOpen,
|
onBatchOpen = onBatchOpen,
|
||||||
|
showSearch = showSearch,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -159,6 +296,7 @@ private fun NormalTopBar(
|
|||||||
onSearchOpen: () -> Unit,
|
onSearchOpen: () -> Unit,
|
||||||
onSort: () -> Unit,
|
onSort: () -> Unit,
|
||||||
onBatchOpen: () -> Unit,
|
onBatchOpen: () -> Unit,
|
||||||
|
showSearch: Boolean = true,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val dark = LocalDarkTheme.current
|
val dark = LocalDarkTheme.current
|
||||||
@@ -180,7 +318,7 @@ private fun NormalTopBar(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
if (!isAlbum) {
|
if (!isAlbum && showSearch) {
|
||||||
NeumorphicIconButton(
|
NeumorphicIconButton(
|
||||||
icon = LlmpIcons.Search,
|
icon = LlmpIcons.Search,
|
||||||
onClick = onSearchOpen,
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import kotlinx.coroutines.delay
|
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.PlayerMusicCover
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.rememberMusicCoverUri
|
import com.zhushenwudi.lovelivemusicplayer.ui.components.rememberMusicCoverUri
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
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.abs
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
@@ -164,9 +167,10 @@ fun FullPlayerScreen(
|
|||||||
if (panelOpen) content = PlayerContent.Cover
|
if (panelOpen) content = PlayerContent.Cover
|
||||||
}
|
}
|
||||||
// 歌词列表:禁用 WeSlide 嵌套/面板拖拽,避免滚动被抢占。
|
// 歌词列表:禁用 WeSlide 嵌套/面板拖拽,避免滚动被抢占。
|
||||||
// 歌词区与进度条之间的顶栏仍通过 onChromeVerticalDrag 驱动关闭。
|
// 横屏右侧始终是歌词,拖拽只留在左侧封面区。
|
||||||
LaunchedEffect(content) {
|
val landscape = LocalIsLandscape.current
|
||||||
AppContainer.weSlide.dragEnabled = content == PlayerContent.Cover
|
LaunchedEffect(content, landscape) {
|
||||||
|
AppContainer.weSlide.dragEnabled = !landscape && content == PlayerContent.Cover
|
||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose { AppContainer.weSlide.dragEnabled = true }
|
onDispose { AppContainer.weSlide.dragEnabled = true }
|
||||||
@@ -190,6 +194,42 @@ fun FullPlayerScreen(
|
|||||||
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Box(Modifier.fillMaxSize().background(bg))
|
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(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -361,7 +401,7 @@ fun FullPlayerScreen(
|
|||||||
background = controlBg,
|
background = controlBg,
|
||||||
)
|
)
|
||||||
NeumorphicIconButton(
|
NeumorphicIconButton(
|
||||||
icon = if (isPlaying) LlmpIcons.Pause else LlmpIcons.Play,
|
icon = if (isPlaying && current != null) LlmpIcons.Pause else LlmpIcons.Play,
|
||||||
onClick = { AppContainer.player.toggle() },
|
onClick = { AppContainer.player.toggle() },
|
||||||
size = 80.dp,
|
size = 80.dp,
|
||||||
iconSize = 26.dp,
|
iconSize = 26.dp,
|
||||||
@@ -393,6 +433,7 @@ fun FullPlayerScreen(
|
|||||||
}
|
}
|
||||||
Spacer(Modifier.height(28.dp))
|
Spacer(Modifier.height(28.dp))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAddToMenu) {
|
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
|
@Composable
|
||||||
private fun PlayerHeader(
|
private fun PlayerHeader(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -599,10 +903,11 @@ private fun PlayerLyricScroll(
|
|||||||
otherColor: Color,
|
otherColor: Color,
|
||||||
onTapBack: () -> Unit,
|
onTapBack: () -> Unit,
|
||||||
onSeek: (positionMs: Long) -> Unit,
|
onSeek: (positionMs: Long) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (lyric.main.isEmpty()) {
|
if (lyric.main.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.pointerInput(onTapBack) {
|
.pointerInput(onTapBack) {
|
||||||
detectTapGestures(onTap = { onTapBack() })
|
detectTapGestures(onTap = { onTapBack() })
|
||||||
@@ -675,7 +980,7 @@ private fun PlayerLyricScroll(
|
|||||||
selectedIndex = -1
|
selectedIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
BoxWithConstraints(modifier.fillMaxSize()) {
|
||||||
// 半视口留白,使首尾行可居中到锚点。
|
// 半视口留白,使首尾行可居中到锚点。
|
||||||
val edgePad = maxHeight / 2
|
val edgePad = maxHeight / 2
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
@@ -865,11 +1170,12 @@ private fun PlayerFuncButtons(
|
|||||||
onToggleLove: () -> Unit,
|
onToggleLove: () -> Unit,
|
||||||
onAddSong: () -> Unit,
|
onAddSong: () -> Unit,
|
||||||
onToggleTranslate: () -> Unit,
|
onToggleTranslate: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val btnBg = Color.Transparent
|
val btnBg = Color.Transparent
|
||||||
val btnIcon = iconTint
|
val btnIcon = iconTint
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -944,13 +1250,13 @@ private fun PlayerFuncButtons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PlayerCoverArt(music: Music?) {
|
private fun PlayerCoverArt(music: Music?, size: Dp = 273.dp) {
|
||||||
val dark = LocalDarkTheme.current
|
val dark = LocalDarkTheme.current
|
||||||
val coverUri = rememberMusicCoverUri(music)
|
val coverUri = rememberMusicCoverUri(music)
|
||||||
val hasCover = !coverUri.isNullOrBlank()
|
val hasCover = !coverUri.isNullOrBlank()
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.size(273.dp)
|
.size(size)
|
||||||
.clip(RoundedCornerShape(24.dp))
|
.clip(RoundedCornerShape(24.dp))
|
||||||
.background(if (dark) ColorMs.Dark2B333A else ColorMs.E7F2FF),
|
.background(if (dark) ColorMs.Dark2B333A else ColorMs.E7F2FF),
|
||||||
contentAlignment = Alignment.Center,
|
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.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -73,6 +74,8 @@ fun MiniPlayerBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
/** 仅选择模式为 0 时可点击封面打开全屏播放器。 */
|
/** 仅选择模式为 0 时可点击封面打开全屏播放器。 */
|
||||||
interactionEnabled: Boolean = true,
|
interactionEnabled: Boolean = true,
|
||||||
|
/** 嵌进底栏右侧时去掉悬浮胶囊,铺满父级高度。 */
|
||||||
|
embedded: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val current by remember {
|
val current by remember {
|
||||||
AppContainer.player.state.map { it.current }.distinctUntilChanged()
|
AppContainer.player.state.map { it.current }.distinctUntilChanged()
|
||||||
@@ -93,27 +96,40 @@ fun MiniPlayerBar(
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(60.dp)
|
|
||||||
.clip(RoundedCornerShape(34.dp))
|
|
||||||
.background(barColor)
|
|
||||||
.then(
|
.then(
|
||||||
if (hazeState != null) {
|
if (embedded) {
|
||||||
Modifier.hazeEffect(state = hazeState) {
|
Modifier.fillMaxHeight()
|
||||||
blurRadius = 30.dp
|
|
||||||
backgroundColor = barColor
|
|
||||||
tints = listOf(
|
|
||||||
HazeTint(barColor),
|
|
||||||
HazeTint(Color.Gray.copy(alpha = 0.15f)),
|
|
||||||
)
|
|
||||||
noiseFactor = 0f
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
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(
|
PlayerMusicCover(
|
||||||
music = current,
|
music = current,
|
||||||
circular = true,
|
circular = true,
|
||||||
fallbackPadding = 8.dp,
|
fallbackPadding = if (embedded) 2.dp else 8.dp,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -185,10 +201,10 @@ fun MiniPlayerBar(
|
|||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
// 新拟态按钮 30×30,图标 15,内边距 5,无阴影
|
// 新拟态按钮 30×30,图标 15,内边距 5,无阴影
|
||||||
MiniBarIconButton(
|
MiniBarIconButton(
|
||||||
asset = if (isPlaying) LlmpIcons.Pause else LlmpIcons.Play,
|
asset = if (isPlaying && current != null) LlmpIcons.Pause else LlmpIcons.Play,
|
||||||
tint = iconColor,
|
tint = iconColor,
|
||||||
contentDescription = if (isPlaying) tr("pause") else tr("play"),
|
contentDescription = if (isPlaying && current != null) tr("pause") else tr("play"),
|
||||||
onClick = if (interactionEnabled) {
|
onClick = if (interactionEnabled && current != null) {
|
||||||
{ AppContainer.player.toggle() }
|
{ AppContainer.player.toggle() }
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
+271
-2
@@ -84,6 +84,7 @@ import com.zhushenwudi.lovelivemusicplayer.lyric.LoadedLyric
|
|||||||
import com.zhushenwudi.lovelivemusicplayer.lyric.LrcParser
|
import com.zhushenwudi.lovelivemusicplayer.lyric.LrcParser
|
||||||
import com.zhushenwudi.lovelivemusicplayer.platform.DetailBackHandler
|
import com.zhushenwudi.lovelivemusicplayer.platform.DetailBackHandler
|
||||||
import com.zhushenwudi.lovelivemusicplayer.platform.PlatformUi
|
import com.zhushenwudi.lovelivemusicplayer.platform.PlatformUi
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.domain.Music
|
||||||
import com.zhushenwudi.lovelivemusicplayer.player.LoopMode
|
import com.zhushenwudi.lovelivemusicplayer.player.LoopMode
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.LlmpAsyncImage
|
import com.zhushenwudi.lovelivemusicplayer.ui.image.LlmpAsyncImage
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.clearLlmpImageCache
|
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.SvgAsset
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.SvgIcon
|
import com.zhushenwudi.lovelivemusicplayer.ui.icons.SvgIcon
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalDesignScale
|
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalDesignScale
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.hSp
|
import com.zhushenwudi.lovelivemusicplayer.ui.theme.hSp
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
@@ -487,9 +489,15 @@ fun TransferScreen(onBack: () -> Unit, onOpenScan: () -> Unit) {
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(bg),
|
.background(bg),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
) {
|
||||||
AppHeader(title = tr("music_trans"), onBack = { requestBack() })
|
AppHeader(title = tr("music_trans"), onBack = { requestBack() })
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
if (connected || state.completed) {
|
if (connected || state.completed) {
|
||||||
Spacer(Modifier.height(35.dp))
|
Spacer(Modifier.height(35.dp))
|
||||||
TransferCoverBlock(
|
TransferCoverBlock(
|
||||||
@@ -552,6 +560,7 @@ fun TransferScreen(onBack: () -> Unit, onOpenScan: () -> Unit) {
|
|||||||
Text(it, color = ColorMs.PinkF940A7, fontSize = hSp(12f))
|
Text(it, color = ColorMs.PinkF940A7, fontSize = hSp(12f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (showLeaveConfirm) {
|
if (showLeaveConfirm) {
|
||||||
TwoButtonDialog(
|
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(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -993,7 +1019,7 @@ fun DriveModeScreen(onBack: () -> Unit) {
|
|||||||
Spacer(Modifier.width(38.dp))
|
Spacer(Modifier.width(38.dp))
|
||||||
// 播放按钮 90×90,外内边距 8,图标无描边环。
|
// 播放按钮 90×90,外内边距 8,图标无描边环。
|
||||||
NeumorphicIconButton(
|
NeumorphicIconButton(
|
||||||
icon = if (state.isPlaying) {
|
icon = if (state.isPlaying && state.current != null) {
|
||||||
SvgAsset("drawable/drive_car_play_button.svg")
|
SvgAsset("drawable/drive_car_play_button.svg")
|
||||||
} else {
|
} else {
|
||||||
SvgAsset("drawable/drive_car_pause_button.svg")
|
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
|
@Composable
|
||||||
private fun DriveBottomItem(
|
private fun DriveBottomItem(
|
||||||
text: String,
|
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.ArtistPhotoImage
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.CoverPlaceholder
|
import com.zhushenwudi.lovelivemusicplayer.ui.components.CoverPlaceholder
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.DetailsPage
|
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.DialogMoreWithMusic
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.DialogSongInfo
|
import com.zhushenwudi.lovelivemusicplayer.ui.components.DialogSongInfo
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.components.ListItemSubtitleLineHeight
|
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.components.addMusicPlayNext
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
import com.zhushenwudi.lovelivemusicplayer.ui.icons.LlmpIcons
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalHomeListBottomInset
|
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalHomeListBottomInset
|
||||||
|
import com.zhushenwudi.lovelivemusicplayer.ui.theme.LocalIsLandscape
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryTabContent(
|
fun LibraryTabContent(
|
||||||
@@ -261,7 +263,11 @@ private fun AlbumList(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(3),
|
columns = if (LocalIsLandscape.current) {
|
||||||
|
GridCells.Adaptive(108.dp)
|
||||||
|
} else {
|
||||||
|
GridCells.Fixed(3)
|
||||||
|
},
|
||||||
state = resolvedGridState,
|
state = resolvedGridState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(
|
contentPadding = PaddingValues(
|
||||||
@@ -502,6 +508,7 @@ fun AlbumDetailScreen(albumId: String, onBack: () -> Unit, onOpenPlayer: () -> U
|
|||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onOpenPlayer = onOpenPlayer,
|
onOpenPlayer = onOpenPlayer,
|
||||||
cover = {
|
cover = {
|
||||||
|
val coverSize = LocalDetailCoverSize.current
|
||||||
AlbumDetailsCover(
|
AlbumDetailsCover(
|
||||||
albumName = album?.albumName.orEmpty(),
|
albumName = album?.albumName.orEmpty(),
|
||||||
category = album?.category,
|
category = album?.category,
|
||||||
@@ -510,9 +517,9 @@ fun AlbumDetailScreen(albumId: String, onBack: () -> Unit, onOpenPlayer: () -> U
|
|||||||
if (album != null) {
|
if (album != null) {
|
||||||
AlbumCoverImage(
|
AlbumCoverImage(
|
||||||
album = album,
|
album = album,
|
||||||
// 图片高度未指定时回退为宽度(240)。
|
// 图片高度未指定时回退为宽度。竖屏 240,横屏随左侧栏缩小。
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(240.dp)
|
.size(coverSize)
|
||||||
.llmpDetailSharedElement(
|
.llmpDetailSharedElement(
|
||||||
HeroKeys.album(albumId),
|
HeroKeys.album(albumId),
|
||||||
HeroRoundedClip,
|
HeroRoundedClip,
|
||||||
@@ -522,7 +529,7 @@ fun AlbumDetailScreen(albumId: String, onBack: () -> Unit, onOpenPlayer: () -> U
|
|||||||
} else {
|
} else {
|
||||||
CoverPlaceholder(
|
CoverPlaceholder(
|
||||||
label = tr("album_abbr"),
|
label = tr("album_abbr"),
|
||||||
size = 240.dp,
|
size = coverSize,
|
||||||
radius = 12.dp,
|
radius = 12.dp,
|
||||||
modifier = Modifier.llmpDetailSharedElement(
|
modifier = Modifier.llmpDetailSharedElement(
|
||||||
HeroKeys.album(albumId),
|
HeroKeys.album(albumId),
|
||||||
@@ -551,10 +558,11 @@ fun ArtistDetailScreen(artistId: String, onBack: () -> Unit, onOpenPlayer: () ->
|
|||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onOpenPlayer = onOpenPlayer,
|
onOpenPlayer = onOpenPlayer,
|
||||||
cover = {
|
cover = {
|
||||||
|
val coverSize = LocalDetailCoverSize.current
|
||||||
if (artist != null) {
|
if (artist != null) {
|
||||||
ArtistPhotoImage(
|
ArtistPhotoImage(
|
||||||
artist = artist,
|
artist = artist,
|
||||||
size = 240.dp,
|
size = coverSize,
|
||||||
modifier = Modifier.llmpDetailSharedElement(
|
modifier = Modifier.llmpDetailSharedElement(
|
||||||
HeroKeys.singer(artistId),
|
HeroKeys.singer(artistId),
|
||||||
HeroCircleClip,
|
HeroCircleClip,
|
||||||
@@ -563,7 +571,7 @@ fun ArtistDetailScreen(artistId: String, onBack: () -> Unit, onOpenPlayer: () ->
|
|||||||
} else {
|
} else {
|
||||||
CoverPlaceholder(
|
CoverPlaceholder(
|
||||||
label = tr("music_abbr"),
|
label = tr("music_abbr"),
|
||||||
size = 240.dp,
|
size = coverSize,
|
||||||
radius = 120.dp,
|
radius = 120.dp,
|
||||||
modifier = Modifier.llmpDetailSharedElement(
|
modifier = Modifier.llmpDetailSharedElement(
|
||||||
HeroKeys.singer(artistId),
|
HeroKeys.singer(artistId),
|
||||||
@@ -603,10 +611,11 @@ fun MenuDetailScreen(menuId: Long, onBack: () -> Unit, onOpenPlayer: () -> Unit)
|
|||||||
songs = AppContainer.library.getMusicByIdsPreservingOrder(nextIds, enableHttp)
|
songs = AppContainer.library.getMusicByIdsPreservingOrder(nextIds, enableHttp)
|
||||||
},
|
},
|
||||||
cover = {
|
cover = {
|
||||||
|
val coverSize = LocalDetailCoverSize.current
|
||||||
if (coverMusic != null) {
|
if (coverMusic != null) {
|
||||||
MusicCoverImage(
|
MusicCoverImage(
|
||||||
music = coverMusic,
|
music = coverMusic,
|
||||||
size = 240.dp,
|
size = coverSize,
|
||||||
radius = 12.dp,
|
radius = 12.dp,
|
||||||
modifier = Modifier.llmpDetailSharedElement(
|
modifier = Modifier.llmpDetailSharedElement(
|
||||||
HeroKeys.menu(menuId),
|
HeroKeys.menu(menuId),
|
||||||
@@ -616,7 +625,7 @@ fun MenuDetailScreen(menuId: Long, onBack: () -> Unit, onOpenPlayer: () -> Unit)
|
|||||||
} else {
|
} else {
|
||||||
CoverPlaceholder(
|
CoverPlaceholder(
|
||||||
label = menu?.name?.take(1).orEmpty().ifEmpty { tr("menu_abbr") },
|
label = menu?.name?.take(1).orEmpty().ifEmpty { tr("menu_abbr") },
|
||||||
size = 240.dp,
|
size = coverSize,
|
||||||
radius = 12.dp,
|
radius = 12.dp,
|
||||||
modifier = Modifier.llmpDetailSharedElement(
|
modifier = Modifier.llmpDetailSharedElement(
|
||||||
HeroKeys.menu(menuId),
|
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.i18n.tr
|
||||||
import com.zhushenwudi.lovelivemusicplayer.theme.ColorMs
|
import com.zhushenwudi.lovelivemusicplayer.theme.ColorMs
|
||||||
import com.zhushenwudi.lovelivemusicplayer.ui.image.LlmpAsyncImage
|
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
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,18 +75,20 @@ fun SplashScreen(onFinished: () -> Unit) {
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.White),
|
.background(Color.White),
|
||||||
) {
|
) {
|
||||||
|
val landscape = LocalIsLandscape.current
|
||||||
val model = photoPath ?: Res.getUri("drawable/launch_background.png")
|
val model = photoPath ?: Res.getUri("drawable/launch_background.png")
|
||||||
LlmpAsyncImage(
|
LlmpAsyncImage(
|
||||||
model = model,
|
model = model,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = if (photoPath != null) ContentScale.Crop else ContentScale.Fit,
|
contentScale = if (photoPath != null && !landscape) ContentScale.Crop else ContentScale.Fit,
|
||||||
alignment = Alignment.TopCenter,
|
alignment = if (landscape) Alignment.Center else Alignment.TopCenter,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.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))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(ColorMs.PinkF940A7.copy(alpha = 100f / 255f))
|
.background(ColorMs.PinkF940A7.copy(alpha = 100f / 255f))
|
||||||
.clickable(
|
.clickable(
|
||||||
|
|||||||
+21
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
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.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -28,6 +30,9 @@ import com.zhushenwudi.lovelivemusicplayer.Const
|
|||||||
*/
|
*/
|
||||||
val LocalDesignScale = staticCompositionLocalOf { 1f }
|
val LocalDesignScale = staticCompositionLocalOf { 1f }
|
||||||
|
|
||||||
|
/** 窗口宽大于高。竖屏组合树不读这个值。 */
|
||||||
|
val LocalIsLandscape = staticCompositionLocalOf { false }
|
||||||
|
|
||||||
/** [com.zhushenwudi.lovelivemusicplayer.ui.theme.ProvideDesignDensity] 缩放前的设备密度(供底部栏使用)。 */
|
/** [com.zhushenwudi.lovelivemusicplayer.ui.theme.ProvideDesignDensity] 缩放前的设备密度(供底部栏使用)。 */
|
||||||
val LocalUnscaledDensity = staticCompositionLocalOf<Density?> { null }
|
val LocalUnscaledDensity = staticCompositionLocalOf<Density?> { null }
|
||||||
|
|
||||||
@@ -62,7 +67,10 @@ fun ProvideDesignDensity(content: @Composable () -> Unit) {
|
|||||||
return@CompositionLocalProvider
|
return@CompositionLocalProvider
|
||||||
}
|
}
|
||||||
val heightDp = heightPx / base.density
|
val heightDp = heightPx / base.density
|
||||||
|
val widthPx = containerPx.width
|
||||||
|
val isLandscape = widthPx > 0 && widthPx > heightPx
|
||||||
// 按高度比例缩放;不低于设计尺寸;平板上限 1.35。忽略系统字体缩放(对齐 Flutter textScaler = 1.0)。
|
// 按高度比例缩放;不低于设计尺寸;平板上限 1.35。忽略系统字体缩放(对齐 Flutter textScaler = 1.0)。
|
||||||
|
// 横屏高度变矮时下限仍是 1,不会按变矮的高度放大或缩小。
|
||||||
val scale = (heightDp / Const.UI_HEIGHT).coerceIn(1f, 1.35f)
|
val scale = (heightDp / Const.UI_HEIGHT).coerceIn(1f, 1.35f)
|
||||||
val scaled = remember(base.density, scale) {
|
val scaled = remember(base.density, scale) {
|
||||||
Density(
|
Density(
|
||||||
@@ -74,6 +82,7 @@ fun ProvideDesignDensity(content: @Composable () -> Unit) {
|
|||||||
LocalDensity provides scaled,
|
LocalDensity provides scaled,
|
||||||
LocalUnscaledDensity provides base,
|
LocalUnscaledDensity provides base,
|
||||||
LocalDesignScale provides scale,
|
LocalDesignScale provides scale,
|
||||||
|
LocalIsLandscape provides isLandscape,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -104,6 +113,18 @@ fun unscaledStatusBarsTop(): Dp = unscaledSystemBarPx(
|
|||||||
startupPx = LocalStartupWindowMetrics.current.statusTopPx,
|
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,使用真实设备密度,忽略设计缩放)。 */
|
/** 导航栏底部内边距(dp,使用真实设备密度,忽略设计缩放)。 */
|
||||||
@Composable
|
@Composable
|
||||||
fun unscaledNavigationBarsBottom(): Dp = unscaledSystemBarPx(
|
fun unscaledNavigationBarsBottom(): Dp = unscaledSystemBarPx(
|
||||||
|
|||||||
@@ -167,10 +167,14 @@
|
|||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -85,8 +85,12 @@ targets:
|
|||||||
- remote-notification
|
- remote-notification
|
||||||
UISupportedInterfaceOrientations:
|
UISupportedInterfaceOrientations:
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
|
- UIInterfaceOrientationLandscapeLeft
|
||||||
|
- UIInterfaceOrientationLandscapeRight
|
||||||
"UISupportedInterfaceOrientations~ipad":
|
"UISupportedInterfaceOrientations~ipad":
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
|
- UIInterfaceOrientationLandscapeLeft
|
||||||
|
- UIInterfaceOrientationLandscapeRight
|
||||||
NSCameraUsageDescription: 需要获取摄像头权限扫描二维码
|
NSCameraUsageDescription: 需要获取摄像头权限扫描二维码
|
||||||
NSPhotoLibraryUsageDescription: 需要读取相册获取图片
|
NSPhotoLibraryUsageDescription: 需要读取相册获取图片
|
||||||
NSAppTransportSecurity:
|
NSAppTransportSecurity:
|
||||||
|
|||||||
+5
@@ -223,6 +223,7 @@ actual class PlayerController(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual fun play() {
|
actual fun play() {
|
||||||
|
if (player.mediaItemCount == 0) return
|
||||||
player.play()
|
player.play()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +232,10 @@ actual class PlayerController(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual fun toggle() {
|
actual fun toggle() {
|
||||||
|
if (player.mediaItemCount == 0) {
|
||||||
|
if (player.isPlaying) pause()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (player.isPlaying) pause() else play()
|
if (player.isPlaying) pause() else play()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -646,7 +646,8 @@ actual class PlayerController {
|
|||||||
actual fun play() {
|
actual fun play() {
|
||||||
ensureAudioReady()
|
ensureAudioReady()
|
||||||
val current = _state.value.current
|
val current = _state.value.current
|
||||||
if (loadedUri.isNullOrBlank() && current != null) {
|
if (current == null) return
|
||||||
|
if (loadedUri.isNullOrBlank()) {
|
||||||
playWhenReady = false
|
playWhenReady = false
|
||||||
AppLog.e(TAG, "play() abort reason=noLoadedUri id=${current.musicId}")
|
AppLog.e(TAG, "play() abort reason=noLoadedUri id=${current.musicId}")
|
||||||
_state.update { it.copy(isPlaying = false, error = "No playable URI") }
|
_state.update { it.copy(isPlaying = false, error = "No playable URI") }
|
||||||
@@ -674,6 +675,10 @@ actual class PlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual fun toggle() {
|
actual fun toggle() {
|
||||||
|
if (_state.value.current == null) {
|
||||||
|
if (_state.value.isPlaying) pause()
|
||||||
|
return
|
||||||
|
}
|
||||||
if (_state.value.isPlaying) pause() else play()
|
if (_state.value.isPlaying) pause() else play()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user