Skip to content

Commit a4e2801

Browse files
committed
fix(video): stabilize portrait playback and recommendations
1 parent a34d454 commit a4e2801

26 files changed

Lines changed: 551 additions & 144 deletions

app/src/main/java/com/android/purebilibili/core/store/SettingsManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5216,6 +5216,8 @@ object SettingsManager {
52165216
private val KEY_PORTRAIT_FULLSCREEN_ENABLED = booleanPreferencesKey("portrait_fullscreen_enabled")
52175217
private val KEY_AUTO_PORTRAIT_FULLSCREEN = booleanPreferencesKey("auto_portrait_fullscreen")
52185218
private val KEY_LAUNCH_TO_PORTRAIT_FEED_ON_STARTUP = booleanPreferencesKey("launch_to_portrait_feed_on_startup")
5219+
private val KEY_PORTRAIT_ONLY_VERTICAL_RECOMMENDATIONS =
5220+
booleanPreferencesKey("portrait_only_vertical_recommendations")
52195221
private const val PORTRAIT_STARTUP_CACHE_PREFS = "portrait_startup_cache"
52205222
private const val CACHE_KEY_LAUNCH_TO_PORTRAIT_FEED = "enabled"
52215223
private val KEY_VERTICAL_VIDEO_RATIO = floatPreferencesKey("vertical_video_ratio")
@@ -5246,6 +5248,18 @@ object SettingsManager {
52465248
context.settingsDataStore.edit { preferences -> preferences[KEY_AUTO_PORTRAIT_FULLSCREEN] = value }
52475249
}
52485250

5251+
/** Whether the portrait pager should exclude landscape recommendations (default disabled). */
5252+
fun getPortraitOnlyVerticalRecommendations(context: Context): Flow<Boolean> =
5253+
context.settingsDataStore.data.map { preferences ->
5254+
preferences[KEY_PORTRAIT_ONLY_VERTICAL_RECOMMENDATIONS] ?: false
5255+
}
5256+
5257+
suspend fun setPortraitOnlyVerticalRecommendations(context: Context, value: Boolean) {
5258+
context.settingsDataStore.edit { preferences ->
5259+
preferences[KEY_PORTRAIT_ONLY_VERTICAL_RECOMMENDATIONS] = value
5260+
}
5261+
}
5262+
52495263
fun getLaunchToPortraitFeedOnStartup(context: Context): Flow<Boolean> = context.settingsDataStore.data
52505264
.map { preferences -> preferences[KEY_LAUNCH_TO_PORTRAIT_FEED_ON_STARTUP] ?: false }
52515265

app/src/main/java/com/android/purebilibili/data/model/response/RelatedResponse.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ data class RelatedVideo(
1818
val stat: Stat = Stat(),
1919
val duration: Int = 0, // 视频时长(秒)
2020
val pubdate: Long = 0,
21+
/** null means the lightweight source did not expose trustworthy dimensions. */
22+
val isVertical: Boolean? = null,
2123
)

app/src/main/java/com/android/purebilibili/data/repository/VideoRepository.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,20 +1099,21 @@ object VideoRepository {
10991099
}
11001100
}
11011101

1102-
suspend fun isVerticalVideo(bvid: String): Boolean = withContext(Dispatchers.IO) {
1102+
suspend fun isVerticalVideo(bvid: String, aid: Long = 0L): Boolean = withContext(Dispatchers.IO) {
11031103
val normalizedBvid = bvid.trim()
1104-
if (normalizedBvid.isEmpty()) return@withContext false
1105-
verticalVideoCache[normalizedBvid]?.let { return@withContext it }
1104+
if (normalizedBvid.isEmpty() && aid <= 0L) return@withContext false
1105+
val cacheKey = normalizedBvid.ifEmpty { "av$aid" }
1106+
verticalVideoCache[cacheKey]?.let { return@withContext it }
11061107
try {
1107-
val lookup = resolveVideoInfoLookupInput(rawBvid = normalizedBvid, aid = 0L)
1108+
val lookup = resolveVideoInfoLookupInput(rawBvid = normalizedBvid, aid = aid)
11081109
?: return@withContext false
11091110
val viewResp = if (lookup.bvid.isNotEmpty()) {
11101111
api.getVideoInfo(lookup.bvid)
11111112
} else {
11121113
api.getVideoInfoByAid(lookup.aid)
11131114
}
11141115
val isVertical = viewResp.data?.dimension?.isVertical == true
1115-
verticalVideoCache[normalizedBvid] = isVertical
1116+
verticalVideoCache[cacheKey] = isVertical
11161117
isVertical
11171118
} catch (_: Exception) {
11181119
false

app/src/main/java/com/android/purebilibili/feature/settings/screen/PlaybackSettingsScreen.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,6 +1554,9 @@ private fun PlaybackFullscreenGestureSettingsSection(
15541554
.getPortraitSwipeToFullscreenEnabled(context).collectAsStateWithLifecycle(initialValue = true)
15551555
val directPortraitStoryEntry by com.android.purebilibili.core.store.SettingsManager
15561556
.getAutoPortraitFullscreen(context).collectAsStateWithLifecycle(initialValue = false)
1557+
val portraitOnlyVerticalRecommendations by com.android.purebilibili.core.store.SettingsManager
1558+
.getPortraitOnlyVerticalRecommendations(context)
1559+
.collectAsStateWithLifecycle(initialValue = false)
15571560
val launchToPortraitFeedOnStartup by com.android.purebilibili.core.store.SettingsManager
15581561
.getLaunchToPortraitFeedOnStartup(context).collectAsStateWithLifecycle(initialValue = false)
15591562
val centerSwipeToFullscreenEnabled by com.android.purebilibili.core.store.SettingsManager
@@ -1862,6 +1865,25 @@ private fun PlaybackFullscreenGestureSettingsSection(
18621865
iconTint = iOSTeal
18631866
)
18641867

1868+
AppPreferenceDivider()
1869+
AppSwitchPreference(
1870+
icon = rememberSettingsSemanticIcon(SettingsIconRole.PORTRAIT_STORY_ENTRY),
1871+
title = "竖屏刷视频仅推荐真竖屏(Beta)",
1872+
subtitle = if (portraitOnlyVerticalRecommendations) {
1873+
"开启后过滤横屏视频,仅保留实际画面为竖屏的推荐"
1874+
} else {
1875+
"关闭后竖屏刷视频允许横竖屏混合推荐"
1876+
},
1877+
checked = portraitOnlyVerticalRecommendations,
1878+
onCheckedChange = {
1879+
scope.launch {
1880+
com.android.purebilibili.core.store.SettingsManager
1881+
.setPortraitOnlyVerticalRecommendations(context, it)
1882+
}
1883+
},
1884+
iconTint = iOSTeal
1885+
)
1886+
18651887
AppPreferenceDivider()
18661888
AppSwitchPreference(
18671889
icon = rememberSettingsSemanticIcon(SettingsIconRole.STARTUP_PORTRAIT_FEED),

app/src/main/java/com/android/purebilibili/feature/story/StoryScreen.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ fun StoryScreen(
5353

5454
val context = LocalContext.current
5555
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
56+
val onlyVerticalRecommendations by com.android.purebilibili.core.store.SettingsManager
57+
.getPortraitOnlyVerticalRecommendations(context)
58+
.collectAsStateWithLifecycle(initialValue = false)
5659
LaunchedEffect(context) {
5760
engagementViewModel.initWithContext(context)
5861
}
@@ -120,6 +123,7 @@ fun StoryScreen(
120123
initialBvid = portraitFeed.initialInfo.bvid,
121124
initialInfo = portraitFeed.initialInfo,
122125
recommendations = portraitFeed.recommendations,
126+
onlyVerticalRecommendations = onlyVerticalRecommendations,
123127
isActive = isActive,
124128
onBack = onBack,
125129
onHomeClick = onBack,

app/src/main/java/com/android/purebilibili/feature/video/screen/VideoDetailPortraitOverlayAdapter.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ internal fun VideoDetailPortraitOverlayAdapter(
125125
}
126126
val portraitOnlyVerticalRecommendations by com.android.purebilibili.core.store.SettingsManager
127127
.getPortraitOnlyVerticalRecommendations(context)
128-
.collectAsStateWithLifecycle(initialValue = true)
128+
.collectAsStateWithLifecycle(initialValue = false)
129129
AnimatedVisibility(
130130
visible = showPortraitFullscreen && success != null,
131131
enter = if (shouldAnimatePortraitPager) {

app/src/main/java/com/android/purebilibili/feature/video/ui/components/CollectionRow.kt

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ fun CollectionRow(
5151
currentBvid: String,
5252
currentCid: Long = 0L,
5353
isPlaying: Boolean = false,
54+
immersive: Boolean = false,
5455
onClick: () -> Unit = {},
5556
modifier: Modifier = Modifier
5657
) {
@@ -78,6 +79,9 @@ fun CollectionRow(
7879
)
7980
val currentPosition = if (currentIndex >= 0) currentIndex + 1 else 0
8081
val totalCount = allEpisodes.size.takeIf { it > 0 } ?: ugcSeason.ep_count
82+
val accentColor = if (immersive) Color.White else MaterialTheme.colorScheme.primary
83+
val titleColor = if (immersive) Color.White.copy(alpha = 0.94f) else MaterialTheme.colorScheme.onSurface
84+
val secondaryColor = if (immersive) Color.White.copy(alpha = 0.78f) else MaterialTheme.colorScheme.onSurfaceVariant
8185

8286
AppSurface(
8387
modifier = modifier
@@ -96,13 +100,13 @@ fun CollectionRow(
96100
modifier = Modifier
97101
.size(32.dp)
98102
.clip(VideoDetailShapes.compactIcon())
99-
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)),
103+
.background(accentColor.copy(alpha = if (immersive) 0.18f else 0.15f)),
100104
contentAlignment = Alignment.Center
101105
) {
102106
AppIcon(
103107
Icons.Outlined.Folder,
104108
contentDescription = null,
105-
tint = MaterialTheme.colorScheme.primary,
109+
tint = accentColor,
106110
modifier = Modifier.size(18.dp)
107111
)
108112
}
@@ -115,21 +119,24 @@ fun CollectionRow(
115119
AppText(
116120
text = "合集",
117121
style = MaterialTheme.typography.labelMedium,
118-
color = MaterialTheme.colorScheme.primary,
122+
color = accentColor,
119123
fontWeight = FontWeight.Medium
120124
)
121125
Spacer(modifier = Modifier.width(6.dp))
122126
AppText(
123127
text = ugcSeason.title,
124128
modifier = Modifier.weight(1f, fill = false),
125129
style = MaterialTheme.typography.bodyMedium,
126-
color = MaterialTheme.colorScheme.onSurface,
130+
color = titleColor,
127131
maxLines = 1,
128132
overflow = TextOverflow.Ellipsis,
129133
fontWeight = FontWeight.Medium
130134
)
131135
Spacer(modifier = Modifier.width(8.dp))
132-
CollectionPlaybackIndicator(isPlaying = isPlaying)
136+
CollectionPlaybackIndicator(
137+
isPlaying = isPlaying,
138+
color = accentColor,
139+
)
133140
}
134141

135142
Spacer(modifier = Modifier.height(4.dp))
@@ -142,15 +149,15 @@ fun CollectionRow(
142149
AppText(
143150
text = "$currentPosition/$totalCount",
144151
style = MaterialTheme.typography.labelMedium,
145-
color = MaterialTheme.colorScheme.onSurfaceVariant,
152+
color = secondaryColor,
146153
fontWeight = FontWeight.Medium
147154
)
148155
}
149156

150157
AppText(
151158
text = resolveCollectionSortLabel(sortMode),
152159
style = MaterialTheme.typography.labelSmall,
153-
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.88f)
160+
color = if (immersive) secondaryColor else accentColor.copy(alpha = 0.88f)
154161
)
155162
}
156163
}
@@ -161,7 +168,8 @@ fun CollectionRow(
161168
collectionId = collectionSubscriptionId,
162169
currentBvid = currentBvid,
163170
currentAid = currentAid,
164-
fontSize = 12.sp
171+
fontSize = 12.sp,
172+
immersive = immersive,
165173
)
166174

167175
// 分享按钮
@@ -181,15 +189,15 @@ fun CollectionRow(
181189
shareIcon,
182190
contentDescription = "分享合集",
183191
modifier = Modifier.size(16.dp),
184-
tint = MaterialTheme.colorScheme.primary
192+
tint = accentColor.copy(alpha = if (immersive) 0.9f else 1f)
185193
)
186194
}
187195

188196
// 右侧箭头
189197
AppIcon(
190198
Icons.AutoMirrored.Outlined.KeyboardArrowRight,
191199
contentDescription = "查看合集",
192-
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
200+
tint = secondaryColor.copy(alpha = if (immersive) 0.8f else 0.5f),
193201
modifier = Modifier.size(16.dp)
194202
)
195203
}
@@ -200,9 +208,9 @@ fun CollectionRow(
200208
@Composable
201209
private fun CollectionPlaybackIndicator(
202210
isPlaying: Boolean,
211+
color: Color,
203212
modifier: Modifier = Modifier,
204213
) {
205-
val color = MaterialTheme.colorScheme.primary
206214
val progress = if (isPlaying) {
207215
val transition = rememberInfiniteTransition(label = "collectionPlayback")
208216
val animatedProgress by transition.animateFloat(

app/src/main/java/com/android/purebilibili/feature/video/ui/components/CollectionSubscriptionButton.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.runtime.remember
1414
import androidx.compose.runtime.rememberCoroutineScope
1515
import androidx.compose.runtime.setValue
1616
import androidx.compose.ui.platform.LocalContext
17+
import androidx.compose.ui.graphics.Color
1718
import androidx.compose.ui.text.font.FontWeight
1819
import androidx.compose.ui.unit.TextUnit
1920
import androidx.compose.ui.unit.dp
@@ -27,6 +28,7 @@ internal fun CollectionSubscriptionButton(
2728
currentBvid: String,
2829
currentAid: Long,
2930
fontSize: TextUnit,
31+
immersive: Boolean = false,
3032
contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp, vertical = 4.dp)
3133
) {
3234
val context = LocalContext.current
@@ -92,7 +94,9 @@ internal fun CollectionSubscriptionButton(
9294
) {
9395
AppText(
9496
text = if (isSubscribed) "已订阅" else "订阅",
95-
color = if (isSubscribed) {
97+
color = if (immersive) {
98+
Color.White.copy(alpha = if (isSubscribed) 0.92f else 0.78f)
99+
} else if (isSubscribed) {
96100
MaterialTheme.colorScheme.primary
97101
} else {
98102
MaterialTheme.colorScheme.onSurfaceVariant

app/src/main/java/com/android/purebilibili/feature/video/ui/components/PlayerMiuixListPopup.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ private val PlayerEndBottomPopupPositionProvider = object : PopupPositionProvide
7373
fun PlayerMiuixListPopup(
7474
title: String,
7575
onDismissRequest: () -> Unit,
76+
modifier: Modifier = Modifier,
7677
placement: PlayerListPopupPlacement = PlayerListPopupPlacement.CENTER,
7778
maxHeight: Dp? = 440.dp,
7879
minWidth: Dp = ListPopupDefaults.MinWidth,
@@ -121,7 +122,7 @@ fun PlayerMiuixListPopup(
121122
},
122123
) {
123124
AppSurface(
124-
modifier = Modifier
125+
modifier = modifier
125126
.widthIn(min = minWidth, max = 320.dp)
126127
.then(if (maxHeight != null) Modifier.heightIn(max = maxHeight) else Modifier),
127128
) { popupContent() }
@@ -132,6 +133,7 @@ fun PlayerMiuixListPopup(
132133

133134
WindowListPopup(
134135
show = true,
136+
popupModifier = modifier,
135137
popupPositionProvider = when (placement) {
136138
PlayerListPopupPlacement.CENTER -> PlayerCenterPopupPositionProvider
137139
PlayerListPopupPlacement.END_BOTTOM -> PlayerEndBottomPopupPositionProvider

app/src/main/java/com/android/purebilibili/feature/video/ui/components/VideoAspectRatio.kt

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ package com.android.purebilibili.feature.video.ui.components
33
import com.android.purebilibili.core.ui.components.AppIcon
44
import com.android.purebilibili.core.ui.components.AppText
55

6-
import androidx.compose.foundation.background
7-
import androidx.compose.foundation.clickable
86
import androidx.compose.foundation.layout.*
9-
import androidx.compose.foundation.shape.RoundedCornerShape
107
import androidx.compose.material.icons.Icons
118
import androidx.compose.material.icons.outlined.*
129
import androidx.compose.material.icons.filled.*
@@ -15,7 +12,6 @@ import androidx.compose.runtime.*
1512
import androidx.compose.ui.Alignment
1613
import androidx.compose.ui.Modifier
1714
import androidx.compose.ui.graphics.Color
18-
import androidx.compose.ui.text.font.FontWeight
1915
import androidx.compose.ui.unit.dp
2016
import androidx.compose.ui.unit.sp
2117
import androidx.media3.ui.AspectRatioFrameLayout
@@ -236,6 +232,7 @@ fun AspectRatioMenu(
236232
PlayerMiuixListPopup(
237233
title = "画面比例",
238234
onDismissRequest = onDismiss,
235+
modifier = modifier,
239236
placement = PlayerListPopupPlacement.END,
240237
minWidth = 160.dp,
241238
) {
@@ -254,51 +251,6 @@ fun AspectRatioMenu(
254251
)
255252
}
256253
}
257-
return
258-
259-
AppSurface(
260-
modifier = modifier.widthIn(min = 120.dp, max = 200.dp),
261-
shape = AppShapes.container(ContainerLevel.Card),
262-
color = Color.Black.copy(alpha = 0.85f),
263-
shadowElevation = 8.dp
264-
) {
265-
Column(
266-
modifier = Modifier.padding(8.dp),
267-
horizontalAlignment = Alignment.CenterHorizontally
268-
) {
269-
// 标题
270-
AppText(
271-
text = "画面比例",
272-
color = Color.White.copy(alpha = 0.7f),
273-
fontSize = 12.sp,
274-
modifier = Modifier.padding(bottom = 8.dp)
275-
)
276-
277-
// 比例选项
278-
VideoAspectRatio.entries.forEach { ratio ->
279-
val isSelected = ratio == currentRatio
280-
AppSurface(
281-
modifier = Modifier
282-
.fillMaxWidth()
283-
.padding(vertical = 2.dp),
284-
shape = AppShapes.container(ContainerLevel.Chip),
285-
color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) else Color.Transparent,
286-
onClick = {
287-
onRatioSelected(ratio)
288-
onDismiss()
289-
}
290-
) {
291-
AppText(
292-
text = ratio.displayName,
293-
color = if (isSelected) MaterialTheme.colorScheme.primary else Color.White,
294-
fontSize = 14.sp,
295-
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
296-
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp)
297-
)
298-
}
299-
}
300-
}
301-
}
302254
}
303255

304256
/**

0 commit comments

Comments
 (0)