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