10
10
package me.him188.ani.app.ui.exploration.search
11
11
12
12
import androidx.compose.animation.AnimatedVisibility
13
- import androidx.compose.foundation.focusGroup
14
- import androidx.compose.foundation.gestures.animateScrollBy
15
- import androidx.compose.foundation.layout.Arrangement
16
- import androidx.compose.foundation.layout.Box
13
+ import androidx.compose.foundation.background
17
14
import androidx.compose.foundation.layout.Column
18
15
import androidx.compose.foundation.layout.WindowInsets
19
16
import androidx.compose.foundation.layout.WindowInsetsSides
20
17
import androidx.compose.foundation.layout.fillMaxWidth
21
18
import androidx.compose.foundation.layout.only
22
19
import androidx.compose.foundation.layout.padding
23
- import androidx.compose.foundation.layout.size
24
- import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
25
20
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
26
- import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
27
- import androidx.compose.foundation.relocation.BringIntoViewRequester
28
- import androidx.compose.foundation.relocation.bringIntoViewRequester
29
21
import androidx.compose.material.icons.Icons
30
22
import androidx.compose.material.icons.rounded.KeyboardArrowUp
31
23
import androidx.compose.material3.Icon
@@ -34,54 +26,30 @@ import androidx.compose.material3.Scaffold
34
26
import androidx.compose.material3.SmallFloatingActionButton
35
27
import androidx.compose.material3.Text
36
28
import androidx.compose.material3.TopAppBarDefaults
29
+ import androidx.compose.material3.TopAppBarScrollBehavior
37
30
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
38
31
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
39
32
import androidx.compose.runtime.Composable
40
- import androidx.compose.runtime.DisposableEffect
41
- import androidx.compose.runtime.LaunchedEffect
42
33
import androidx.compose.runtime.getValue
43
- import androidx.compose.runtime.mutableIntStateOf
44
- import androidx.compose.runtime.mutableStateMapOf
45
- import androidx.compose.runtime.mutableStateOf
46
- import androidx.compose.runtime.remember
47
34
import androidx.compose.runtime.rememberCoroutineScope
48
- import androidx.compose.runtime.saveable.rememberSaveable
49
- import androidx.compose.runtime.setValue
50
- import androidx.compose.runtime.snapshotFlow
51
35
import androidx.compose.ui.Modifier
52
36
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
53
37
import androidx.compose.ui.input.nestedscroll.nestedScroll
54
- import androidx.compose.ui.layout.onSizeChanged
55
- import androidx.compose.ui.unit.Dp
56
38
import androidx.compose.ui.unit.dp
57
39
import androidx.lifecycle.compose.collectAsStateWithLifecycle
58
- import androidx.paging.compose.LazyPagingItems
59
- import androidx.paging.compose.itemContentType
60
- import androidx.paging.compose.itemKey
61
40
import kotlinx.coroutines.CoroutineStart
62
- import kotlinx.coroutines.flow.collectLatest
63
41
import kotlinx.coroutines.launch
64
- import me.him188.ani.app.data.models.preference.NsfwMode
65
42
import me.him188.ani.app.navigation.LocalNavigator
66
43
import me.him188.ani.app.ui.adaptive.AniListDetailPaneScaffold
67
44
import me.him188.ani.app.ui.adaptive.AniTopAppBar
68
45
import me.him188.ani.app.ui.adaptive.PaneScope
69
46
import me.him188.ani.app.ui.foundation.LocalPlatform
70
- import me.him188.ani.app.ui.foundation.animation.LocalAniMotionScheme
71
47
import me.him188.ani.app.ui.foundation.ifThen
72
- import me.him188.ani.app.ui.foundation.interaction.keyboardDirectionToSelectItem
73
- import me.him188.ani.app.ui.foundation.interaction.keyboardPageToScroll
74
48
import me.him188.ani.app.ui.foundation.layout.AniWindowInsets
75
49
import me.him188.ani.app.ui.foundation.layout.currentWindowAdaptiveInfo1
76
50
import me.him188.ani.app.ui.foundation.layout.isWidthAtLeastMedium
77
- import me.him188.ani.app.ui.foundation.layout.paneHorizontalPadding
78
- import me.him188.ani.app.ui.foundation.layout.paneVerticalPadding
79
51
import me.him188.ani.app.ui.foundation.navigation.BackHandler
80
52
import me.him188.ani.app.ui.foundation.widgets.BackNavigationIconButton
81
- import me.him188.ani.app.ui.foundation.widgets.NsfwMask
82
- import me.him188.ani.app.ui.search.LoadErrorCard
83
- import me.him188.ani.app.ui.search.SearchDefaults
84
- import me.him188.ani.app.ui.search.SearchResultLazyVerticalStaggeredGrid
85
53
import me.him188.ani.app.ui.search.collectHasQueryAsState
86
54
import me.him188.ani.utils.platform.isDesktop
87
55
@@ -104,34 +72,22 @@ fun SearchPage(
104
72
val scope = rememberCoroutineScope()
105
73
106
74
val items = state.items
107
- val searchBar = @Composable {
108
- Column (verticalArrangement = Arrangement .spacedBy(16 .dp)) {
75
+ SearchPageListDetailScaffold (
76
+ navigator,
77
+ searchBar = {
109
78
SuggestionSearchBar (
110
79
state.suggestionSearchBarState,
111
80
Modifier
112
81
.ifThen(
113
82
currentWindowAdaptiveInfo1().windowSizeClass.isWidthAtLeastMedium
114
- || ! state.suggestionSearchBarState.expanded,
115
- ) { Modifier .padding(horizontal = 8 .dp) },
83
+ && ! state.suggestionSearchBarState.expanded,
84
+ ) { Modifier .padding(horizontal = 8 .dp) } // from 16 to 24
85
+ .padding(bottom = 16 .dp),
116
86
placeholder = { Text (" 搜索" ) },
87
+ windowInsets = contentWindowInsets.only(WindowInsetsSides .Horizontal ),
117
88
)
118
-
119
- val filterState by state.searchFilterStateFlow.collectAsStateWithLifecycle()
120
- SearchFilterChipsRow (
121
- filterState,
122
- onClickItemText = { chip, value ->
123
- state.toggleTagSelection(chip, value, unselectOthersOfSameKind = true )
124
- },
125
- onCheckedChange = { chip, value ->
126
- state.toggleTagSelection(chip, value, unselectOthersOfSameKind = false )
127
- },
128
- Modifier .fillMaxWidth(),
129
- )
130
- }
131
- }
132
- SearchPageLayout (
133
- navigator,
134
- searchResultList = { nestedScrollConnection ->
89
+ },
90
+ searchResultColumn = { nestedScrollConnection ->
135
91
val aniNavigator = LocalNavigator .current
136
92
137
93
val hasQuery by state.searchState.collectHasQueryAsState()
@@ -152,17 +108,25 @@ fun SearchPage(
152
108
}
153
109
}
154
110
}, // collect only once
155
- searchBar = searchBar,
111
+ headers = {
112
+ item(span = StaggeredGridItemSpan .FullLine ) {
113
+ val filterState by state.searchFilterStateFlow.collectAsStateWithLifecycle()
114
+ SearchFilterChipsRow (
115
+ filterState,
116
+ onClickItemText = { chip, value ->
117
+ state.toggleTagSelection(chip, value, unselectOthersOfSameKind = true )
118
+ },
119
+ onCheckedChange = { chip, value ->
120
+ state.toggleTagSelection(chip, value, unselectOthersOfSameKind = false )
121
+ },
122
+ Modifier .fillMaxWidth(),
123
+ )
124
+ }
125
+ },
156
126
state = state.gridState,
157
127
)
158
128
},
159
129
detailContent = {
160
- // AnimatedContent(
161
- // state.selectedItemIndex,
162
- // transitionSpec = AniThemeDefaults.emphasizedAnimatedContentTransition,
163
- // ) { index ->
164
- //
165
- // }
166
130
items.itemSnapshotList.getOrNull(state.selectedItemIndex)?.let {
167
131
detailContent(it.subjectId)
168
132
}
@@ -200,157 +164,29 @@ fun SearchPage(
200
164
)
201
165
}
202
166
203
- @Composable
204
- internal fun SearchPageResultColumn (
205
- items : LazyPagingItems <SubjectPreviewItemInfo >,
206
- showSummary : () -> Boolean , // 可在还没发起任何搜索时不展示
207
- selectedItemIndex : () -> Int ,
208
- onSelect : (index: Int ) -> Unit ,
209
- onPlay : (info: SubjectPreviewItemInfo ) -> Unit ,
210
- searchBar : @Composable () -> Unit ,
211
- modifier : Modifier = Modifier ,
212
- state : LazyStaggeredGridState = rememberLazyStaggeredGridState()
213
- ) {
214
- var height by rememberSaveable { mutableIntStateOf(0 ) }
215
- val bringIntoViewRequesters = remember { mutableStateMapOf<Int , BringIntoViewRequester >() }
216
- val nsfwBlurShape = SubjectItemLayoutParameters .calculate(currentWindowAdaptiveInfo1().windowSizeClass).shape
217
- val aniMotionScheme = LocalAniMotionScheme .current
218
-
219
- SearchResultLazyVerticalStaggeredGrid (
220
- items,
221
- error = {
222
- LoadErrorCard (
223
- error = it,
224
- onRetry = { items.retry() },
225
- modifier = Modifier .fillMaxWidth(), // noop
226
- )
227
- },
228
- modifier
229
- .focusGroup()
230
- .onSizeChanged { height = it.height }
231
- .keyboardDirectionToSelectItem(
232
- selectedItemIndex,
233
- ) {
234
- onSelect(it)
235
- state.animateScrollToItem(it)
236
- }
237
- .keyboardPageToScroll({ height.toFloat() }) {
238
- state.animateScrollBy(it)
239
- },
240
- lazyStaggeredGridState = state,
241
- horizontalArrangement = Arrangement .spacedBy(currentWindowAdaptiveInfo1().windowSizeClass.paneHorizontalPadding),
242
- ) {
243
- item(span = StaggeredGridItemSpan .FullLine ) {
244
- searchBar()
245
- }
246
-
247
- if (showSummary()) {
248
- item(span = StaggeredGridItemSpan .FullLine ) {
249
- SearchDefaults .SearchSummaryItem (
250
- items,
251
- Modifier .animateItem(
252
- fadeInSpec = aniMotionScheme.feedItemFadeInSpec,
253
- placementSpec = aniMotionScheme.feedItemPlacementSpec,
254
- fadeOutSpec = aniMotionScheme.feedItemFadeOutSpec,
255
- ),
256
- containerColor = MaterialTheme .colorScheme.surfaceContainerLowest,
257
- )
258
- }
259
- }
260
-
261
- items(
262
- items.itemCount,
263
- key = items.itemKey { it.subjectId },
264
- contentType = items.itemContentType { 1 },
265
- ) { index ->
266
- val info = items[index]
267
- val requester = remember { BringIntoViewRequester () }
268
- // 记录 item 对应的 requester
269
- if (info != null ) {
270
- DisposableEffect (requester) {
271
- bringIntoViewRequesters[info.subjectId] = requester
272
- onDispose {
273
- bringIntoViewRequesters.remove(info.subjectId)
274
- }
275
- }
276
-
277
- var nsfwMaskState: NsfwMode by rememberSaveable(info) {
278
- mutableStateOf(info.nsfwMode)
279
- }
280
- NsfwMask (
281
- mode = nsfwMaskState,
282
- onTemporarilyDisplay = { nsfwMaskState = NsfwMode .DISPLAY },
283
- shape = nsfwBlurShape,
284
- ) {
285
- SubjectPreviewItem (
286
- selected = index == selectedItemIndex(),
287
- onClick = { onSelect(index) },
288
- onPlay = { onPlay(info) },
289
- info = info,
290
- Modifier
291
- // .sharedElement(
292
- // rememberSharedContentState(SharedTransitionKeys.subjectBounds(info.subjectId)),
293
- // animatedVisibilityScope,
294
- // )
295
- .animateItem(
296
- fadeInSpec = aniMotionScheme.feedItemFadeInSpec,
297
- placementSpec = aniMotionScheme.feedItemPlacementSpec,
298
- fadeOutSpec = aniMotionScheme.feedItemFadeOutSpec,
299
- )
300
- .fillMaxWidth()
301
- .bringIntoViewRequester(requester)
302
- .padding(vertical = currentWindowAdaptiveInfo1().windowSizeClass.paneVerticalPadding / 2 ),
303
- image = {
304
- SubjectItemDefaults .Image (
305
- info.imageUrl,
306
- // Modifier.sharedElement(
307
- // rememberSharedContentState(SharedTransitionKeys.subjectCoverImage(subjectId = info.subjectId)),
308
- // animatedVisibilityScope,
309
- // ),
310
- )
311
- },
312
- title = { maxLines ->
313
- Text (
314
- info.title,
315
- // Modifier.sharedElement(
316
- // rememberSharedContentState(SharedTransitionKeys.subjectTitle(subjectId = info.subjectId)),
317
- // animatedVisibilityScope,
318
- // ),
319
- maxLines = maxLines,
320
- )
321
- },
322
- )
323
- }
324
- } else {
325
- Box (Modifier .size(Dp .Hairline ))
326
- // placeholder
327
- }
328
- }
329
- }
330
-
331
- LaunchedEffect (Unit ) {
332
- snapshotFlow(selectedItemIndex)
333
- .collectLatest {
334
- bringIntoViewRequesters[items.itemSnapshotList.getOrNull(it)?.subjectId]?.bringIntoView()
335
- }
336
- }
337
- }
338
-
339
167
/* *
340
168
* @param searchBar contentPadding: 页面的左右 24.dp 边距
341
169
*/
342
170
@Composable
343
- internal fun SearchPageLayout (
171
+ internal fun SearchPageListDetailScaffold (
344
172
navigator : ThreePaneScaffoldNavigator <* >,
345
- searchResultList : @Composable (PaneScope .(NestedScrollConnection ) -> Unit ),
173
+ searchBar : @Composable (PaneScope .() -> Unit ),
174
+ searchResultColumn : @Composable (PaneScope .(NestedScrollConnection ? ) -> Unit ),
346
175
detailContent : @Composable (PaneScope .() -> Unit ),
347
176
navigateToTopButton : @Composable PaneScope .() -> Unit ,
348
177
modifier : Modifier = Modifier ,
349
178
navigationIcon : @Composable () -> Unit = {},
350
179
contentWindowInsets : WindowInsets = AniWindowInsets .forPageContent(),
351
180
) {
352
181
val coroutineScope = rememberCoroutineScope()
353
- val topAppBarScrollBehavior = TopAppBarDefaults .exitUntilCollapsedScrollBehavior()
182
+
183
+ val topAppBarScrollBehavior: TopAppBarScrollBehavior ? = if (LocalPlatform .current.isDesktop()) {
184
+ // Workaround for Compose bug: scrolling to the top does not work correctly.
185
+ null
186
+ } else {
187
+ TopAppBarDefaults .exitUntilCollapsedScrollBehavior()
188
+ }
189
+
354
190
AniListDetailPaneScaffold (
355
191
navigator,
356
192
listPaneTopAppBar = {
@@ -371,9 +207,7 @@ internal fun SearchPageLayout(
371
207
}
372
208
},
373
209
windowInsets = paneContentWindowInsets.only(WindowInsetsSides .Top + WindowInsetsSides .Horizontal ),
374
- scrollBehavior =
375
- if (LocalPlatform .current.isDesktop()) null // Workaround for Compose bug: scrolling to the top does not work correctly.
376
- else topAppBarScrollBehavior,
210
+ scrollBehavior = topAppBarScrollBehavior,
377
211
)
378
212
},
379
213
listPaneContent = {
@@ -385,18 +219,22 @@ internal fun SearchPageLayout(
385
219
Modifier
386
220
.paneContentPadding()
387
221
.paneWindowInsetsPadding()
388
- .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
222
+ .run {
223
+ if (topAppBarScrollBehavior == null ) this
224
+ else nestedScroll(topAppBarScrollBehavior.nestedScrollConnection)
225
+ },
389
226
) {
390
- searchResultList(topAppBarScrollBehavior.nestedScrollConnection)
227
+ searchBar()
228
+ searchResultColumn(topAppBarScrollBehavior?.nestedScrollConnection)
391
229
}
392
230
}
393
231
},
394
232
detailPane = {
395
233
detailContent()
396
234
},
397
- modifier,
235
+ modifier.background( MaterialTheme .colorScheme.surfaceContainerLowest) ,
398
236
useSharedTransition = false ,
399
- listPanePreferredWidth = 480 .dp,
237
+ listPanePreferredWidth = 400 .dp,
400
238
contentWindowInsets = contentWindowInsets,
401
239
)
402
240
}
0 commit comments