From 53897bda1ac8b40a51109e477f62709e16e70318 Mon Sep 17 00:00:00 2001 From: zhushenwudi <55681140@163.com> Date: Sat, 3 Oct 2026 19:07:41 +0800 Subject: [PATCH] optimize orientation container position --- .../ui/components/AppDrawer.kt | 11 ++- .../ui/player/FullPlayerScreen.kt | 90 ++++++++++++----- .../ui/theme/DesignDensity.kt | 11 ++- iosApp/iosApp.xcodeproj/project.pbxproj | 99 ++++++++++--------- .../lyric/PlatformInfo.ios.kt | 47 ++++++++- 5 files changed, 174 insertions(+), 84 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/AppDrawer.kt b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/AppDrawer.kt index 8e4b5e4..269d92e 100644 --- a/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/AppDrawer.kt +++ b/composeApp/src/commonMain/kotlin/com/zhushenwudi/lovelivemusicplayer/ui/components/AppDrawer.kt @@ -68,7 +68,7 @@ private object DrawerLayout { val headerToGroupsGap = 10.dp val groupRowGap = 12.dp val groupsToMenuGap = 10.dp - val groupButtonWidth = 120.dp + // 宽度不固定:按容器宽度两等分,才能和功能菜单的左右边距对齐。设计稿为 120dp。 val groupButtonHeight = 50.dp val groupButtonGap = 10.dp val menuOuterPaddingH = 8.dp @@ -221,7 +221,10 @@ fun AppDrawer( horizontalAlignment = Alignment.CenterHorizontally, ) { groupRows.forEachIndexed { index, row -> + // 必须撑满并等分:固定宽度会被外层 Column 的 CenterHorizontally 居中, + // 平板上抽屉变宽后就和下面左对齐的功能菜单差出一截边距。 Row( + Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(DrawerLayout.groupButtonGap), verticalAlignment = Alignment.CenterVertically, ) { @@ -232,7 +235,7 @@ fun AppDrawer( selected = currentGroup == group.key, onClick = { onGroupSelect(group.key) }, modifier = Modifier - .width(DrawerLayout.groupButtonWidth) + .weight(1f) .height(DrawerLayout.groupButtonHeight), ) } @@ -313,7 +316,9 @@ private fun GroupButton( AsyncImage( model = Res.getUri(group.asset.path), contentDescription = group.key.displayName, - contentScale = ContentScale.FillWidth, + // Fit 而非 FillWidth:按钮改为等分后在平板上更宽, + // 撑满宽度会把 logo 顶出 50dp 的高度被裁掉。 + contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize(), ) } 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 c3c67b5..f3430a0 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 @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed @@ -467,6 +468,23 @@ fun FullPlayerScreen( } } +/** 横屏左栏整体右移一些,不要贴着屏幕边。 */ +private val LandscapeBodyStartPadding = 48.dp + +/** 横屏封面上限。平板上可用高度能到 280dp 以上,卡住它免得封面占满整栏。 */ +private val LandscapeCoverMaxSize = 250.dp + +/** 封面区再内缩一点,连带下方同宽的内容一起偏右。 */ +private val LandscapeCoverInsetStart = 24.dp + +/** + * 封面边长未知时先撑满;已知后与封面同宽,左右边缘才对得上。 + * 偏移量要和封面那边保持一致,否则两者又错开。 + */ +private fun Modifier.coverAlignedWidth(side: Dp?): Modifier = + offset(x = LandscapeCoverInsetStart) + .then(if (side != null) width(side) else fillMaxWidth()) + @Composable private fun LandscapePlayerBody( current: Music?, @@ -499,12 +517,15 @@ private fun LandscapePlayerBody( .statusBarsPadding() .navigationBarsPadding() .padding( - start = safeStart + 28.dp, + start = safeStart + LandscapeBodyStartPadding, end = safeEnd + 28.dp, top = 12.dp, bottom = 16.dp, ), ) { + // 封面是受高度约束的正方形,通常比左栏窄;下方内容跟随它的实际边长, + // 否则 fillMaxWidth 会让两者左右边缘差出一截。 + var coverSide by remember { mutableStateOf(null) } Column( Modifier .weight(0.42f) @@ -513,6 +534,7 @@ private fun LandscapePlayerBody( .padding(end = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { + // 收起按钮保持贴左栏边缘,不跟封面对齐。 Row( Modifier.fillMaxWidth().height(40.dp), verticalAlignment = Alignment.CenterVertically, @@ -529,19 +551,22 @@ private fun LandscapePlayerBody( contentDescription = tr("collapse"), ) } - Box( + // 直接用 BoxWithConstraints 承担居中:套在 Box 里再 fillMaxSize 的话, + // 外层的 contentAlignment 不起作用,封面会左上角对齐。 + BoxWithConstraints( Modifier .weight(1f) - .fillMaxWidth(), + .fillMaxWidth() + .offset(x = LandscapeCoverInsetStart), contentAlignment = Alignment.Center, ) { - BoxWithConstraints(Modifier.fillMaxSize()) { - val side = maxWidth - .coerceAtMost(maxHeight) - .coerceAtMost(300.dp) - .coerceAtLeast(1.dp) - PlayerCoverArt(music = current, size = side) - } + val side = maxWidth + .coerceAtMost(maxHeight) + .coerceAtMost(LandscapeCoverMaxSize) + .coerceAtLeast(1.dp) + // 下方内容宽度不影响它们的高度,所以这次写入会立刻收敛。 + if (coverSide != side) coverSide = side + PlayerCoverArt(music = current, size = side) } Text( text = current?.musicName ?: tr("not_playing"), @@ -553,9 +578,7 @@ private fun LandscapePlayerBody( fontSize = 16.sp, fontWeight = FontWeight.Bold, ), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp), + modifier = Modifier.coverAlignedWidth(coverSide), ) val artist = current?.artist.orEmpty() if (artist.isNotBlank()) { @@ -569,8 +592,8 @@ private fun LandscapePlayerBody( fontSize = 12.sp, ), modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 2.dp), + .coverAlignedWidth(coverSide) + .padding(vertical = 2.dp), ) } PlayerFuncButtons( @@ -582,6 +605,8 @@ private fun LandscapePlayerBody( onToggleLove = onToggleLove, onAddSong = onAddSong, onToggleTranslate = onToggleTranslate, + modifier = Modifier.coverAlignedWidth(coverSide), + horizontalPadding = 0.dp, ) PlayerSeekBar( thumbColor = thumbColor, @@ -589,12 +614,14 @@ private fun LandscapePlayerBody( inactiveTrack = inactiveTrack, timeColor = timeColor, onSeek = { AppContainer.player.seekTo(it) }, + modifier = Modifier.coverAlignedWidth(coverSide), + trackInset = 0.dp, + labelInset = 0.dp, ) Row( Modifier - .fillMaxWidth() - .height(64.dp) - .padding(horizontal = 4.dp), + .coverAlignedWidth(coverSide) + .height(64.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -1174,13 +1201,15 @@ private fun PlayerFuncButtons( onAddSong: () -> Unit, onToggleTranslate: () -> Unit, modifier: Modifier = Modifier, + /** 横屏左栏要和控制键左右对齐,传 0。 */ + horizontalPadding: Dp = 16.dp, ) { val btnBg = Color.Transparent val btnIcon = iconTint Row( modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = horizontalPadding), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -1287,6 +1316,11 @@ private fun PlayerSeekBar( inactiveTrack: Color, timeColor: Color, onSeek: (Long) -> Unit, + modifier: Modifier = Modifier, + /** 轨道两端留白。横屏左栏要和控制键对齐,传 0。 */ + trackInset: Dp = 24.dp, + /** 时间文字的左右内边距。 */ + labelInset: Dp = 16.dp, ) { val positionMs = rememberPlayerField { it.positionMs } val durationMs = rememberPlayerField { it.durationMs } @@ -1296,14 +1330,18 @@ private fun PlayerSeekBar( var dragValue by remember { mutableFloatStateOf(0f) } val display = if (dragging) dragValue else positionMs.toFloat().coerceIn(0f, max) val fraction = (display / max).coerceIn(0f, 1f) + val thumbRadius = 10.dp + // 滑块中心至少离边一个半径,否则贴边时会被画到容器外; + // 轨道本身可以贴边,两者的留白因此要分开算。 + val thumbInset = maxOf(trackInset, thumbRadius) - Column(Modifier.fillMaxWidth()) { + Column(modifier.fillMaxWidth()) { BoxWithConstraints( Modifier .fillMaxWidth() .height(48.dp) - .pointerInput(max) { - val inset = 24.dp.toPx() + .pointerInput(max, thumbInset) { + val inset = thumbInset.toPx() fun xToValue(x: Float): Float { val usable = (size.width - inset * 2).coerceAtLeast(1f) return ((x - inset) / usable).coerceIn(0f, 1f) * max @@ -1321,11 +1359,9 @@ private fun PlayerSeekBar( } }, ) { - val thumbRadius = 10.dp - val overlayPad = 24.dp val trackHeight = 4.dp Canvas(Modifier.fillMaxSize()) { - val inset = overlayPad.toPx() + val inset = trackInset.toPx() val y = size.height / 2f val trackW = (size.width - inset * 2f).coerceAtLeast(0f) val top = y - trackHeight.toPx() / 2f @@ -1348,7 +1384,7 @@ private fun PlayerSeekBar( Box( Modifier .align(Alignment.CenterStart) - .offset(x = overlayPad - thumbRadius + (maxWidth - overlayPad * 2) * fraction) + .offset(x = thumbInset - thumbRadius + (maxWidth - thumbInset * 2) * fraction) .size(thumbRadius * 2) .shadow(1.dp, CircleShape) .background(thumbColor, CircleShape), @@ -1357,7 +1393,7 @@ private fun PlayerSeekBar( Row( Modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(horizontal = labelInset), horizontalArrangement = Arrangement.SpaceBetween, ) { Text(formatMs(display.roundToLong()), color = timeColor, fontSize = 12.sp) 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 c35c9ab..6ba1b9b 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 @@ -66,12 +66,15 @@ fun ProvideDesignDensity(content: @Composable () -> Unit) { Box(Modifier.fillMaxSize()) 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) + // 按长边比例缩放;不低于设计尺寸;平板上限 1.35。忽略系统字体缩放(对齐 Flutter textScaler = 1.0)。 + // + // 取长边而不是当前窗口高度,是为了让横竖屏共用同一个 dp 坐标系。 + // 按窗口高度算的话,横屏高度变成短边,scale 会从上限 1.35 掉回 1, + // 同一个 dp 在横屏下物理尺寸要小四分之一。 + val longestDp = maxOf(widthPx, heightPx) / base.density + val scale = (longestDp / Const.UI_HEIGHT).coerceIn(1f, 1.35f) val scaled = remember(base.density, scale) { Density( density = base.density * scale, diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 868725c..61bbeb4 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -13,19 +13,19 @@ 5A902F589276392B4CBC0015 /* HomeWidgetExampleBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30942AA1A2E3A1AB832DD959 /* HomeWidgetExampleBundle.swift */; }; 76ED3117C0B97CD5572EB808 /* HomeWidgetExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B1CCBD46108D407DF0C9A3 /* HomeWidgetExample.swift */; }; 8070A4B4DD1D3DD823A8F923 /* WidgetAppUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0142951DE661E55DC3207327 /* WidgetAppUtils.swift */; }; - B7C8D9E0F1A234567890ABCD /* InAppWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6789012345678 /* InAppWebViewController.swift */; }; - C8D9E0F1A2B345678901BCDE /* WebViewContentBlocker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C3D4E5F67890123456789 /* WebViewContentBlocker.swift */; }; - A1B2C3D4E5F60718293A4B5C /* BackgroundIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59CDB1C6A666078B3272B409 /* BackgroundIntent.swift */; }; 88ABACE0259DE83261737B99 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1285E8A7B7E7359EA11B9591 /* SceneDelegate.swift */; }; 93FF51B22016695CB74F0AD6 /* HostBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6B3C131D45A3E52B69B531 /* HostBridge.swift */; }; + A1B2C3D4E5F60718293A4B5C /* BackgroundIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59CDB1C6A666078B3272B409 /* BackgroundIntent.swift */; }; A59815941DC5A1C2B9F349CB /* PipScreenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2016716493FA6F4B6E30EB17 /* PipScreenManager.swift */; }; + B7C8D9E0F1A234567890ABCD /* InAppWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6789012345678 /* InAppWebViewController.swift */; }; C8353E6C8BE98C27FD768A59 /* WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E6AF7C77765E074EE1C7E597 /* WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - F9A23E2F5F7514E5D6A3D9C0 /* BackgroundIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59CDB1C6A666078B3272B409 /* BackgroundIntent.swift */; }; - FA6C12E55F9D41237B4BDAF1 /* CarPlaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310E847101B97B7A36776025 /* CarPlaySceneDelegate.swift */; }; - FDE52B8D57A097A60A4CF366 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6C1F485E0CF8BB70C9DDD2 /* AppDelegate.swift */; }; + C8D9E0F1A2B345678901BCDE /* WebViewContentBlocker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C3D4E5F67890123456789 /* WebViewContentBlocker.swift */; }; D1E2F3A4B5C60718293A4B02 /* QqShareHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E2F3A4B5C60718293A4B01 /* QqShareHost.swift */; }; D1E2F3A4B5C60718293A4B04 /* TencentOpenAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E2F3A4B5C60718293A4B03 /* TencentOpenAPI.framework */; }; D1E2F3A4B5C60718293A4B06 /* TencentOpenApi_IOS_Bundle.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D1E2F3A4B5C60718293A4B05 /* TencentOpenApi_IOS_Bundle.bundle */; }; + F9A23E2F5F7514E5D6A3D9C0 /* BackgroundIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59CDB1C6A666078B3272B409 /* BackgroundIntent.swift */; }; + FA6C12E55F9D41237B4BDAF1 /* CarPlaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310E847101B97B7A36776025 /* CarPlaySceneDelegate.swift */; }; + FDE52B8D57A097A60A4CF366 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B6C1F485E0CF8BB70C9DDD2 /* AppDelegate.swift */; }; I18N0PLIST0BUILD0FILE001 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = I18N0PLIST0VARNT0GROUP01 /* InfoPlist.strings */; }; /* End PBXBuildFile section */ @@ -55,8 +55,6 @@ /* Begin PBXFileReference section */ 0142951DE661E55DC3207327 /* WidgetAppUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetAppUtils.swift; sourceTree = ""; }; - A1B2C3D4E5F6789012345678 /* InAppWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewController.swift; sourceTree = ""; }; - B2C3D4E5F67890123456789 /* WebViewContentBlocker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewContentBlocker.swift; sourceTree = ""; }; 1285E8A7B7E7359EA11B9591 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 2016716493FA6F4B6E30EB17 /* PipScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PipScreenManager.swift; sourceTree = ""; }; 30942AA1A2E3A1AB832DD959 /* HomeWidgetExampleBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWidgetExampleBundle.swift; sourceTree = ""; }; @@ -66,20 +64,33 @@ 86038AE1032E284E79B32AD6 /* iosApp.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 88A292AA7607F14E8C66291E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 8B6C1F485E0CF8BB70C9DDD2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A1B2C3D4E5F6789012345678 /* InAppWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppWebViewController.swift; sourceTree = ""; }; + B2C3D4E5F67890123456789 /* WebViewContentBlocker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewContentBlocker.swift; sourceTree = ""; }; B8B1CCBD46108D407DF0C9A3 /* HomeWidgetExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWidgetExample.swift; sourceTree = ""; }; CE8509E5A49E4A5CAAEB3ADB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - E6AF7C77765E074EE1C7E597 /* WidgetExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - F543015C899423D4B01CEB2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D1E2F3A4B5C60718293A4B01 /* QqShareHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QqShareHost.swift; sourceTree = ""; }; D1E2F3A4B5C60718293A4B03 /* TencentOpenAPI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TencentOpenAPI.framework; path = Vendor/TencentOpenAPI.framework; sourceTree = ""; }; D1E2F3A4B5C60718293A4B05 /* TencentOpenApi_IOS_Bundle.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = TencentOpenApi_IOS_Bundle.bundle; path = Vendor/TencentOpenApi_IOS_Bundle.bundle; sourceTree = ""; }; - I18N0PLIST0FILE0REF0ZH01 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; + D1E2F3A4B5C60718293A4B07 /* iosApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iosApp-Bridging-Header.h"; sourceTree = ""; }; + E6AF7C77765E074EE1C7E597 /* WidgetExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + F543015C899423D4B01CEB2E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + I18N0PLIST0FILE0REF0DE01 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; I18N0PLIST0FILE0REF0EN01 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; I18N0PLIST0FILE0REF0JA01 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; - I18N0PLIST0FILE0REF0DE01 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; - D1E2F3A4B5C60718293A4B07 /* iosApp-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iosApp-Bridging-Header.h"; sourceTree = ""; }; + I18N0PLIST0FILE0REF0ZH01 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFrameworksBuildPhase section */ + D1E2F3A4B5C60718293A4B08 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D1E2F3A4B5C60718293A4B04 /* TencentOpenAPI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + /* Begin PBXGroup section */ 0ECDA917A71DFD183A635257 /* CarPlay */ = { isa = PBXGroup; @@ -119,15 +130,6 @@ ); sourceTree = ""; }; - D1E2F3A4B5C60718293A4B09 /* Vendor */ = { - isa = PBXGroup; - children = ( - D1E2F3A4B5C60718293A4B03 /* TencentOpenAPI.framework */, - D1E2F3A4B5C60718293A4B05 /* TencentOpenApi_IOS_Bundle.bundle */, - ); - name = Vendor; - sourceTree = ""; - }; 5B4BD3733F8124940306FE19 /* pip */ = { isa = PBXGroup; children = ( @@ -156,6 +158,15 @@ path = WidgetExtension; sourceTree = ""; }; + D1E2F3A4B5C60718293A4B09 /* Vendor */ = { + isa = PBXGroup; + children = ( + D1E2F3A4B5C60718293A4B03 /* TencentOpenAPI.framework */, + D1E2F3A4B5C60718293A4B05 /* TencentOpenApi_IOS_Bundle.bundle */, + ); + name = Vendor; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -261,14 +272,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - D1E2F3A4B5C60718293A4B08 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D1E2F3A4B5C60718293A4B04 /* TencentOpenAPI.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -289,7 +292,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -euo pipefail\ncd \"$SRCROOT/..\"\nif [ \"YES\" = \"${OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED:-}\" ]; then\n echo \"Kotlin framework was built by the IDE; syncing Compose resources only\"\n ./gradlew -Pcompose.ios.resources.archs=arm64 :composeApp:syncComposeResourcesForIos --rerun\n exit 0\nfi\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode"; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\nset -euo pipefail\ncd \"$SRCROOT/..\"\nif [ \"YES\" = \"${OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED:-}\" ]; then\n echo \"Kotlin framework was built by the IDE; syncing Compose resources only\"\n ./gradlew -Pcompose.ios.resources.archs=arm64 :composeApp:syncComposeResourcesForIos --rerun\n exit 0\nfi\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode"; }; C516F4DE7FC107135868F830 /* Verify Widget Extension Embedded */ = { isa = PBXShellScriptBuildPhase; @@ -312,20 +315,6 @@ }; /* End PBXShellScriptBuildPhase section */ -/* Begin PBXVariantGroup section */ - I18N0PLIST0VARNT0GROUP01 /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - I18N0PLIST0FILE0REF0ZH01 /* zh-Hans */, - I18N0PLIST0FILE0REF0EN01 /* en */, - I18N0PLIST0FILE0REF0JA01 /* ja */, - I18N0PLIST0FILE0REF0DE01 /* de */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin PBXSourcesBuildPhase section */ 297B59D9CE634556D6007C40 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -365,6 +354,20 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + I18N0PLIST0VARNT0GROUP01 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + I18N0PLIST0FILE0REF0ZH01 /* zh-Hans */, + I18N0PLIST0FILE0REF0EN01 /* en */, + I18N0PLIST0FILE0REF0JA01 /* ja */, + I18N0PLIST0FILE0REF0DE01 /* de */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 11814011E7D6EC822BFBC66D /* Release */ = { isa = XCBuildConfiguration; @@ -475,12 +478,12 @@ WebKit, "-lsqlite3", ); - SWIFT_OBJC_BRIDGING_HEADER = "iosApp/iosApp-Bridging-Header.h"; PRODUCT_BUNDLE_IDENTIFIER = com.zhushenwudi.lovelivemusicplayer; PRODUCT_NAME = LLMP; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "LLMP-M-AD-HOC"; SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "iosApp/iosApp-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Profile; @@ -728,12 +731,12 @@ WebKit, "-lsqlite3", ); - SWIFT_OBJC_BRIDGING_HEADER = "iosApp/iosApp-Bridging-Header.h"; PRODUCT_BUNDLE_IDENTIFIER = com.zhushenwudi.lovelivemusicplayer; PRODUCT_NAME = LLMP; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "LLMP-M-DEVELOP"; SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "iosApp/iosApp-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -777,12 +780,12 @@ WebKit, "-lsqlite3", ); - SWIFT_OBJC_BRIDGING_HEADER = "iosApp/iosApp-Bridging-Header.h"; PRODUCT_BUNDLE_IDENTIFIER = com.zhushenwudi.lovelivemusicplayer; PRODUCT_NAME = LLMP; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "LLMP-M-RELEASE"; SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "iosApp/iosApp-Bridging-Header.h"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/shared/src/iosMain/kotlin/com/zhushenwudi/lovelivemusicplayer/lyric/PlatformInfo.ios.kt b/shared/src/iosMain/kotlin/com/zhushenwudi/lovelivemusicplayer/lyric/PlatformInfo.ios.kt index 5007c8e..dc830ee 100644 --- a/shared/src/iosMain/kotlin/com/zhushenwudi/lovelivemusicplayer/lyric/PlatformInfo.ios.kt +++ b/shared/src/iosMain/kotlin/com/zhushenwudi/lovelivemusicplayer/lyric/PlatformInfo.ios.kt @@ -1,12 +1,55 @@ package com.zhushenwudi.lovelivemusicplayer.lyric +import kotlinx.cinterop.BooleanVar +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.value import platform.Foundation.NSBundle +import platform.Foundation.NSDocumentDirectory +import platform.Foundation.NSFileManager +import platform.Foundation.NSSearchPathForDirectoriesInDomains +import platform.Foundation.NSUserDomainMask + +/** + * 解锁目录名。Info.plist 开了 UIFileSharingEnabled, + * 用户可在「文件」App 的本应用目录下自行新建它。 + */ +private const val UNLOCK_DIR_NAME = "LLMP" actual object PlatformInfo { - // iOS 在 EULA 未接受前可能限制日文/罗马音歌词。 - actual val allowEulaLyric: Boolean = true + /** + * iOS 默认只提供中文歌词;Documents 下存在 LLMP 目录才解锁日文/罗马音。 + * + * 刻意只在首次读取时查一次盘:[LyricService.lrcType] 的初值、歌词下载分支、 + * 以及多处 UI 分支都依赖这个值,运行中途翻转会让它们对不上 + * (例如 lrcType 停在 0 要显示日文,而日文根本没下载过)。 + * 用户新建目录后重启应用生效。 + */ + actual val allowEulaLyric: Boolean by lazy { unlockDirExists() } actual val isIos: Boolean = true actual val appVersion: String = NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? String ?: "" } + +@OptIn(ExperimentalForeignApi::class) +private fun unlockDirExists(): Boolean { + val docs = NSSearchPathForDirectoriesInDomains( + NSDocumentDirectory, + NSUserDomainMask, + true, + ).firstOrNull() as? String ?: return false + val path = if (docs.endsWith("/")) { + "$docs$UNLOCK_DIR_NAME" + } else { + "$docs/$UNLOCK_DIR_NAME" + } + return memScoped { + val isDir = alloc() + // 必须是目录:同名文件不算解锁。 + NSFileManager.defaultManager.fileExistsAtPath(path, isDirectory = isDir.ptr) && + isDir.value + } +}