Skip to content

Commit 84e9c4f

Browse files
committed
fix: align video search sorting and pagination with PiliPlus
1 parent 6427e02 commit 84e9c4f

7 files changed

Lines changed: 47 additions & 143 deletions

File tree

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

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@ package com.android.purebilibili.data.repository
33
import com.android.purebilibili.data.model.response.SearchType
44
import com.android.purebilibili.data.model.response.VideoItem
55

6-
// all/v2 cannot preserve video sorting or filters. Never silently replace a filtered page.
7-
internal fun canFallbackVideoSearch(
8-
order: SearchOrder,
9-
duration: SearchDuration,
10-
tids: Int,
11-
pubBegin: Long?,
12-
pubEnd: Long?
13-
): Boolean = order == SearchOrder.TOTALRANK && duration == SearchDuration.ALL &&
14-
tids == 0 && pubBegin == null && pubEnd == null
15-
166
internal fun resolveVideoSearchPageInfo(
177
requestedPage: Int,
188
responsePage: Int,
@@ -33,17 +23,12 @@ internal fun resolveVideoSearchPageInfo(
3323
currentPage = currentPage,
3424
totalPages = resolvedTotalPages,
3525
totalResults = totalResults.takeIf { it > 0 } ?: resultCount,
36-
hasMore = resultCount > 0 && currentPage < resolvedTotalPages
26+
// Match PiliPlus CommonListController: totals are informational; only an
27+
// empty server page ends pagination, even after a short nonempty page.
28+
hasMore = resultCount > 0
3729
)
3830
}
3931

40-
internal fun shouldFallbackEmptyFirstPageVideoSearch(
41-
page: Int,
42-
primaryResultCount: Int
43-
): Boolean {
44-
return page == 1 && primaryResultCount == 0
45-
}
46-
4732
internal fun resolveSearchLoadedPage(
4833
requestedPage: Int,
4934
responsePage: Int

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

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,20 @@ object SearchRepository {
8181
pubBegin: Long? = null,
8282
pubEnd: Long? = null
8383
): Result<Pair<List<VideoItem>, SearchPageInfo>> = withContext(Dispatchers.IO) {
84-
val allowFallback = canFallbackVideoSearch(order, duration, tids, pubBegin, pubEnd)
8584
try {
8685
val params = mutableMapOf(
8786
"keyword" to keyword,
8887
"search_type" to "video",
8988
"order" to order.value,
9089
"duration" to duration.value.toString(),
91-
"tids" to tids.toString(),
9290
"page" to page.toString(),
9391
"page_size" to "20",
9492
"platform" to "pc",
9593
"web_location" to "1430654"
9694
)
95+
if (tids != 0) {
96+
params["tids"] = tids.toString()
97+
}
9798
if (pubBegin != null) {
9899
params["pubtime_begin_s"] = pubBegin.toString()
99100
}
@@ -110,26 +111,12 @@ object SearchRepository {
110111

111112
val response = api.search(signedParams)
112113
if (response.code != 0) {
113-
if (!allowFallback) {
114-
return@withContext Result.failure(createSearchError(response.code, response.message))
115-
}
116-
com.android.purebilibili.core.util.Logger.w(
117-
"SearchRepo",
118-
"search(video) primary api failed: code=${response.code}, msg=${response.message}, fallback=all/v2"
119-
)
120-
return@withContext searchVideoFallback(keyword = keyword, page = page)
114+
return@withContext Result.failure(createSearchError(response.code, response.message))
121115
}
122-
116+
123117
val videoList = response.data?.result
124118
?.map { it.toVideoItem() }
125119
?: emptyList()
126-
if (allowFallback && shouldFallbackEmptyFirstPageVideoSearch(page = page, primaryResultCount = videoList.size)) {
127-
com.android.purebilibili.core.util.Logger.d(
128-
"SearchRepo",
129-
" search(video) primary first page empty, fallback=all/v2"
130-
)
131-
return@withContext searchVideoFallback(keyword = keyword, page = page)
132-
}
133120
val pageInfo = resolveVideoSearchPageInfo(
134121
requestedPage = page,
135122
responsePage = response.data?.page ?: page,
@@ -148,14 +135,8 @@ object SearchRepository {
148135
} catch (e: CancellationException) {
149136
throw e
150137
} catch (e: Exception) {
151-
if (!allowFallback) return@withContext Result.failure(e)
152-
e.printStackTrace()
153-
com.android.purebilibili.core.util.Logger.e(
154-
"SearchRepo",
155-
"search(video) primary api exception, fallback=all/v2",
156-
e
157-
)
158-
searchVideoFallback(keyword = keyword, page = page)
138+
com.android.purebilibili.core.util.Logger.e("SearchRepo", "search(video) failed", e)
139+
Result.failure(e)
159140
}
160141
}
161142

@@ -697,55 +678,6 @@ object SearchRepository {
697678
}
698679
}
699680

700-
private suspend fun searchVideoFallback(
701-
keyword: String,
702-
page: Int
703-
): Result<Pair<List<VideoItem>, SearchPageInfo>> {
704-
return withContext(Dispatchers.IO) {
705-
try {
706-
val response = api.searchAll(
707-
signWithWbi(
708-
mapOf(
709-
"keyword" to keyword,
710-
"page" to page.toString(),
711-
"page_size" to "20",
712-
"platform" to "pc",
713-
"web_location" to "1430654"
714-
)
715-
)
716-
)
717-
if (response.code != 0) {
718-
return@withContext Result.failure(createSearchError(response.code, response.message))
719-
}
720-
721-
val videos = response.data?.result
722-
?.firstOrNull { it.result_type == "video" }
723-
?.data
724-
?.map { it.toVideoItem() }
725-
?: emptyList()
726-
727-
val pageInfo = SearchPageInfo(
728-
currentPage = page,
729-
totalPages = response.data?.numPages?.takeIf { it > 0 } ?: if (videos.size >= 20) page + 1 else page,
730-
totalResults = response.data?.numResults?.takeIf { it > 0 } ?: videos.size,
731-
hasMore = response.data?.numPages?.let { page < it } ?: (videos.size >= 20)
732-
)
733-
734-
com.android.purebilibili.core.util.Logger.d(
735-
"SearchRepo",
736-
"search(video) fallback result: size=${videos.size}, page=${pageInfo.currentPage}, hasMore=${pageInfo.hasMore}"
737-
)
738-
739-
Result.success(Pair(videos, pageInfo))
740-
} catch (e: CancellationException) {
741-
throw e
742-
} catch (e: Exception) {
743-
e.printStackTrace()
744-
Result.failure(e)
745-
}
746-
}
747-
}
748-
749681
private fun createSearchError(code: Int, message: String): Exception {
750682
val readable = when (code) {
751683
-412 -> "搜索请求被拦截,请稍后重试"

app/src/main/java/com/android/purebilibili/feature/search/SearchScreen.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,17 +2755,17 @@ fun SearchFilterBar(
27552755
if (SearchFilterControl.VIDEO_ORDER in filterControls) {
27562756
Box {
27572757
FilterMenuChip(
2758-
text = currentOrder.displayName,
2758+
text = resolveSearchOrderChipLabel(currentOrder),
27592759
highlighted = currentOrder != SearchOrder.TOTALRANK,
27602760
onClick = { showOrderMenu = true }
27612761
)
27622762
AppDropdownMenu(
27632763
expanded = showOrderMenu,
27642764
onDismissRequest = { showOrderMenu = false }
27652765
) {
2766-
SearchOrder.entries.forEach { order ->
2766+
resolveSearchVideoOrderOptions().forEach { order ->
27672767
AppDropdownMenuItem(
2768-
text = { AppText(order.displayName) },
2768+
text = { AppText(resolveSearchOrderChipLabel(order)) },
27692769
onClick = {
27702770
onOrderChange(order)
27712771
showOrderMenu = false

app/src/main/java/com/android/purebilibili/feature/search/SearchVideoFilterPolicy.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ fun resolveSearchVideoOrderOptions(): List<SearchOrder> {
6969
SearchOrder.CLICK,
7070
SearchOrder.PUBDATE,
7171
SearchOrder.DM,
72-
SearchOrder.STOW
72+
SearchOrder.STOW,
73+
SearchOrder.SCORES
7374
)
7475
}
7576

app/src/main/java/com/android/purebilibili/feature/search/SearchVideoFilterSheet.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,19 @@ fun SearchVideoFilterBar(
102102
onSelected = { index ->
103103
orderOptions.getOrNull(index)?.let(onOrderChange)
104104
},
105-
modifier = Modifier.weight(1f),
105+
modifier = Modifier
106+
.weight(1f)
107+
.horizontalScroll(rememberScrollState())
108+
.width(72.dp * orderOptions.size),
106109
height = AppChromeSizeTokens.BottomBarMatchedSegmentedControlHeightDp.dp,
107110
indicatorHeight = AppChromeSizeTokens.BottomBarMatchedSegmentedIndicatorHeightDp.dp,
108111
labelFontSize = 13.sp,
109112
allowNativeLabelOverflow = true,
110113
miuixBackdrop = miuixBackdrop,
111114
liquidGlassEffectsEnabled = true,
112115
tapPressRefractionEnabled = true,
113-
dragSelectionEnabled = orderOptions.size > 1,
116+
// Horizontal swipes scroll the six sorting labels, as in PiliPlus.
117+
dragSelectionEnabled = false,
114118
)
115119
VerticalDivider(
116120
modifier = Modifier

app/src/test/java/com/android/purebilibili/data/repository/SearchLoadPolicyTest.kt

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,19 @@ import kotlin.test.assertTrue
1010

1111
class SearchLoadPolicyTest {
1212

13-
@Test
14-
fun `fallback is only allowed for unfiltered default video search`() {
15-
assertTrue(canFallbackVideoSearch(SearchOrder.TOTALRANK, SearchDuration.ALL, 0, null, null))
16-
SearchOrder.entries.filter { it != SearchOrder.TOTALRANK }.forEach { order ->
17-
assertFalse(canFallbackVideoSearch(order, SearchDuration.ALL, 0, null, null))
18-
}
19-
assertFalse(canFallbackVideoSearch(SearchOrder.TOTALRANK, SearchDuration.UNDER_10MIN, 0, null, null))
20-
assertFalse(canFallbackVideoSearch(SearchOrder.TOTALRANK, SearchDuration.ALL, 1, null, null))
21-
assertFalse(canFallbackVideoSearch(SearchOrder.TOTALRANK, SearchDuration.ALL, 0, 100L, null))
22-
assertFalse(canFallbackVideoSearch(SearchOrder.TOTALRANK, SearchDuration.ALL, 0, null, 200L))
23-
}
24-
2513
@Test
2614
fun `video pagination uses result total when page total is absent`() {
2715
val first = resolveVideoSearchPageInfo(1, 1, 0, 41, 20, 20)
2816
assertEquals(3, first.totalPages)
2917
assertTrue(first.hasMore)
30-
assertFalse(resolveVideoSearchPageInfo(3, 3, 0, 41, 20, 1).hasMore)
18+
assertTrue(resolveVideoSearchPageInfo(3, 3, 0, 41, 20, 1).hasMore)
3119
}
3220

3321
@Test
34-
fun `full video page without totals can continue but short page ends`() {
22+
fun `both full and short video pages without totals can continue`() {
3523
assertTrue(resolveVideoSearchPageInfo(2, 1, 0, 0, 20, 20).hasMore)
3624
assertEquals(2, resolveVideoSearchPageInfo(2, 1, 0, 0, 20, 20).currentPage)
37-
assertFalse(resolveVideoSearchPageInfo(2, 2, 0, 0, 20, 2).hasMore)
25+
assertTrue(resolveVideoSearchPageInfo(2, 2, 0, 0, 20, 2).hasMore)
3826
}
3927

4028
@Test
@@ -43,38 +31,18 @@ class SearchLoadPolicyTest {
4331
}
4432

4533
@Test
46-
fun `empty server page stops even when pagination metadata claims more`() {
47-
assertFalse(resolveVideoSearchPageInfo(2, 2, 9, 180, 20, 0).hasMore)
48-
}
49-
50-
@Test
51-
fun `first page video search retries fallback when primary result is empty`() {
52-
assertTrue(
53-
shouldFallbackEmptyFirstPageVideoSearch(
54-
page = 1,
55-
primaryResultCount = 0
56-
)
57-
)
58-
}
59-
60-
@Test
61-
fun `later pages do not retry fallback when primary result is empty`() {
62-
assertFalse(
63-
shouldFallbackEmptyFirstPageVideoSearch(
64-
page = 2,
65-
primaryResultCount = 0
66-
)
67-
)
34+
fun `thirteen sorted results probe next page like PiliPlus before ending`() {
35+
val first = resolveVideoSearchPageInfo(1, 1, 1, 13, 20, 13)
36+
assertTrue(first.hasMore)
37+
assertEquals(13, first.totalResults)
38+
val next = resolveVideoSearchPageInfo(2, 2, 1, 13, 20, 0)
39+
assertFalse(next.hasMore)
40+
assertEquals(2, next.currentPage)
6841
}
6942

7043
@Test
71-
fun `non empty first page keeps primary result`() {
72-
assertFalse(
73-
shouldFallbackEmptyFirstPageVideoSearch(
74-
page = 1,
75-
primaryResultCount = 1
76-
)
77-
)
44+
fun `empty server page stops even when pagination metadata claims more`() {
45+
assertFalse(resolveVideoSearchPageInfo(2, 2, 9, 180, 20, 0).hasMore)
7846
}
7947

8048
@Test

app/src/test/java/com/android/purebilibili/feature/search/SearchVideoFilterPolicyTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,20 @@ import kotlin.test.assertTrue
1313

1414
class SearchVideoFilterPolicyTest {
1515

16+
@Test
17+
fun videoOrderOptions_matchPiliPlusLabelsAndApiOrder() {
18+
val options = resolveSearchVideoOrderOptions()
19+
assertEquals(
20+
listOf("默认排序", "播放多", "新发布", "弹幕多", "收藏多", "评论多"),
21+
options.map(::resolveSearchOrderChipLabel)
22+
)
23+
assertEquals(
24+
listOf("totalrank", "click", "pubdate", "dm", "stow", "scores"),
25+
options.map { it.value }
26+
)
27+
assertFalse(SearchOrder.ATTENTION in options)
28+
}
29+
1630
@Test
1731
fun orderChipLabels_matchBiliPai() {
1832
assertEquals("默认排序", resolveSearchOrderChipLabel(SearchOrder.TOTALRANK))

0 commit comments

Comments
 (0)