1
+ package dev.aaa1115910.bv.component.index
2
+
3
+ import android.content.Context
4
+ import androidx.compose.animation.AnimatedVisibility
5
+ import androidx.compose.foundation.layout.Arrangement
6
+ import androidx.compose.foundation.layout.Column
7
+ import androidx.compose.foundation.layout.PaddingValues
8
+ import androidx.compose.foundation.layout.Row
9
+ import androidx.compose.foundation.layout.fillMaxSize
10
+ import androidx.compose.foundation.layout.fillMaxWidth
11
+ import androidx.compose.foundation.layout.heightIn
12
+ import androidx.compose.foundation.layout.padding
13
+ import androidx.compose.foundation.layout.size
14
+ import androidx.compose.material.icons.Icons
15
+ import androidx.compose.material.icons.filled.Check
16
+ import androidx.compose.material3.AlertDialog
17
+ import androidx.compose.material3.HorizontalDivider
18
+ import androidx.compose.material3.Icon
19
+ import androidx.compose.runtime.Composable
20
+ import androidx.compose.runtime.LaunchedEffect
21
+ import androidx.compose.runtime.getValue
22
+ import androidx.compose.runtime.mutableStateOf
23
+ import androidx.compose.runtime.remember
24
+ import androidx.compose.runtime.setValue
25
+ import androidx.compose.ui.Alignment
26
+ import androidx.compose.ui.Modifier
27
+ import androidx.compose.ui.focus.FocusRequester
28
+ import androidx.compose.ui.focus.focusRequester
29
+ import androidx.compose.ui.platform.LocalContext
30
+ import androidx.compose.ui.tooling.preview.Preview
31
+ import androidx.compose.ui.unit.dp
32
+ import androidx.compose.ui.window.DialogProperties
33
+ import androidx.tv.foundation.lazy.list.TvLazyColumn
34
+ import androidx.tv.foundation.lazy.list.TvLazyRow
35
+ import androidx.tv.foundation.lazy.list.items
36
+ import androidx.tv.material3.ExperimentalTvMaterial3Api
37
+ import androidx.tv.material3.FilterChip
38
+ import androidx.tv.material3.MaterialTheme
39
+ import androidx.tv.material3.Surface
40
+ import androidx.tv.material3.Text
41
+ import dev.aaa1115910.biliapi.http.entity.index.IndexOrder
42
+ import dev.aaa1115910.biliapi.http.entity.index.animeIndexOrders
43
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterArea
44
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterCopyright
45
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterIsFinish
46
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterSeasonMonth
47
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterSeasonStatus
48
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterSeasonVersion
49
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterSpokenLanguageType
50
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterStyleIdsAnime
51
+ import dev.aaa1115910.biliapi.http.entity.index.indexFilterYear
52
+ import dev.aaa1115910.bv.R
53
+ import dev.aaa1115910.bv.component.createCustomInitialFocusRestorerModifiers
54
+ import dev.aaa1115910.bv.component.ifElse
55
+ import dev.aaa1115910.bv.ui.theme.BVTheme
56
+
57
+ @OptIn(ExperimentalTvMaterial3Api ::class )
58
+ @Composable
59
+ fun AnimeIndexFilter (
60
+ modifier : Modifier = Modifier ,
61
+ show : Boolean ,
62
+ onDismissRequest : () -> Unit ,
63
+ order : IndexOrder ,
64
+ seasonVersion : Int ,
65
+ spokenLanguageType : Int ,
66
+ area : Int ,
67
+ isFinish : Int ,
68
+ copyright : Int ,
69
+ seasonStatus : Int ,
70
+ seasonMonth : Int ,
71
+ year : String ,
72
+ styleId : Int ,
73
+ desc : Boolean ,
74
+ onOrderChange : (IndexOrder ) -> Unit ,
75
+ onSeasonVersionChange : (Int ) -> Unit ,
76
+ onSpokenLanguageTypeChange : (Int ) -> Unit ,
77
+ onAreaChange : (Int ) -> Unit ,
78
+ onIsFinishChange : (Int ) -> Unit ,
79
+ onCopyrightChange : (Int ) -> Unit ,
80
+ onSeasonStatusChange : (Int ) -> Unit ,
81
+ onSeasonMonthChange : (Int ) -> Unit ,
82
+ onYearChange : (String ) -> Unit ,
83
+ onStyleIdChange : (Int ) -> Unit ,
84
+ onDescChange : (Boolean ) -> Unit ,
85
+ // TODO 重置筛选条件
86
+ onReset : () -> Unit = {}
87
+ ) {
88
+ val context = LocalContext .current
89
+ val focusRequester = remember { FocusRequester () }
90
+
91
+ LaunchedEffect (show) {
92
+ runCatching {
93
+ if (show) focusRequester.requestFocus()
94
+ }
95
+ }
96
+
97
+ if (show) {
98
+ AlertDialog (
99
+ modifier = modifier
100
+ .fillMaxWidth(0.8f ),
101
+ onDismissRequest = onDismissRequest,
102
+ confirmButton = { },
103
+ title = {
104
+ Text (text = " 番剧索引筛选" )
105
+ },
106
+ text = {
107
+ Column (
108
+ modifier = Modifier
109
+ .heightIn(max = 300 .dp)
110
+ ) {
111
+ IndexFilterChipRow (
112
+ modifier = Modifier .focusRequester(focusRequester),
113
+ title = " 排序方式" ,
114
+ filter = animeIndexOrders.associateWith { it.getDisplayName(context) },
115
+ selectedFilterId = order,
116
+ onFilterIdChange = onOrderChange
117
+ )
118
+ IndexFilterChipRow (
119
+ title = " 排序顺序" ,
120
+ filter = mapOf (true to " 降序" , false to " 升序" ),
121
+ selectedFilterId = desc,
122
+ onFilterIdChange = onDescChange
123
+ )
124
+ HorizontalDivider (
125
+ modifier = Modifier .padding(vertical = 8 .dp)
126
+ )
127
+ TvLazyColumn {
128
+ item {
129
+ IndexFilterChipRow (
130
+ title = " 类型" ,
131
+ filter = indexFilterSeasonVersion,
132
+ selectedFilterId = seasonVersion,
133
+ onFilterIdChange = onSeasonVersionChange
134
+ )
135
+ }
136
+ item {
137
+ IndexFilterChipRow (
138
+ title = " 配音" ,
139
+ filter = indexFilterSpokenLanguageType,
140
+ selectedFilterId = spokenLanguageType,
141
+ onFilterIdChange = onSpokenLanguageTypeChange
142
+ )
143
+ }
144
+ item {
145
+ IndexFilterChipRow (
146
+ title = " 地区" ,
147
+ filter = indexFilterArea,
148
+ selectedFilterId = area,
149
+ onFilterIdChange = onAreaChange
150
+ )
151
+ }
152
+ item {
153
+ IndexFilterChipRow (
154
+ title = " 状态" ,
155
+ filter = indexFilterIsFinish,
156
+ selectedFilterId = isFinish,
157
+ onFilterIdChange = onIsFinishChange
158
+ )
159
+ }
160
+ item {
161
+ IndexFilterChipRow (
162
+ title = " 版权" ,
163
+ filter = indexFilterCopyright,
164
+ selectedFilterId = copyright,
165
+ onFilterIdChange = onCopyrightChange
166
+ )
167
+ }
168
+ item {
169
+ IndexFilterChipRow (
170
+ title = " 付费" ,
171
+ filter = indexFilterSeasonStatus,
172
+ selectedFilterId = seasonStatus,
173
+ onFilterIdChange = onSeasonStatusChange
174
+ )
175
+ }
176
+ item {
177
+ IndexFilterChipRow (
178
+ title = " 季度" ,
179
+ filter = indexFilterSeasonMonth,
180
+ selectedFilterId = seasonMonth,
181
+ onFilterIdChange = onSeasonMonthChange
182
+ )
183
+ }
184
+ item {
185
+ IndexFilterChipRow (
186
+ title = " 年份" ,
187
+ filter = indexFilterYear,
188
+ selectedFilterId = year,
189
+ onFilterIdChange = onYearChange
190
+ )
191
+ }
192
+ item {
193
+ IndexFilterChipRow (
194
+ title = " 风格" ,
195
+ filter = indexFilterStyleIdsAnime,
196
+ selectedFilterId = styleId,
197
+ onFilterIdChange = onStyleIdChange
198
+ )
199
+ }
200
+ }
201
+ }
202
+ },
203
+ properties = DialogProperties (usePlatformDefaultWidth = false )
204
+
205
+ )
206
+ }
207
+ }
208
+
209
+ @OptIn(ExperimentalTvMaterial3Api ::class )
210
+ @Preview(device = " id:tv_1080p" )
211
+ @Composable
212
+ private fun AnimeIndexFilterPreview () {
213
+ var order by remember { mutableStateOf(IndexOrder .PlayCount ) }
214
+ var seasonVersion by remember { mutableStateOf(- 1 ) }
215
+ var spokenLanguageType by remember { mutableStateOf(- 1 ) }
216
+ var area by remember { mutableStateOf(- 1 ) }
217
+ var isFinish by remember { mutableStateOf(- 1 ) }
218
+ var copyright by remember { mutableStateOf(- 1 ) }
219
+ var seasonStatus by remember { mutableStateOf(- 1 ) }
220
+ var seasonMonth by remember { mutableStateOf(- 1 ) }
221
+ var year by remember { mutableStateOf(" -1" ) }
222
+ var styleId by remember { mutableStateOf(- 1 ) }
223
+ var desc by remember { mutableStateOf(true ) }
224
+
225
+ BVTheme {
226
+ Surface (
227
+ modifier = Modifier .fillMaxSize()
228
+ ) {
229
+ AnimeIndexFilter (
230
+ show = true ,
231
+ onDismissRequest = { },
232
+ order = order,
233
+ seasonVersion = seasonVersion,
234
+ spokenLanguageType = spokenLanguageType,
235
+ area = area,
236
+ isFinish = isFinish,
237
+ copyright = copyright,
238
+ seasonStatus = seasonStatus,
239
+ seasonMonth = seasonMonth,
240
+ year = year,
241
+ styleId = styleId,
242
+ desc = desc,
243
+ onOrderChange = { order = it },
244
+ onSeasonVersionChange = { seasonVersion = it },
245
+ onSpokenLanguageTypeChange = { spokenLanguageType = it },
246
+ onAreaChange = { area = it },
247
+ onIsFinishChange = { isFinish = it },
248
+ onCopyrightChange = { copyright = it },
249
+ onSeasonStatusChange = { seasonStatus = it },
250
+ onSeasonMonthChange = { seasonMonth = it },
251
+ onYearChange = { year = it },
252
+ onStyleIdChange = { styleId = it },
253
+ onDescChange = { desc = it },
254
+ onReset = { }
255
+ )
256
+ }
257
+ }
258
+ }
259
+
260
+ @OptIn(ExperimentalTvMaterial3Api ::class )
261
+ @Composable
262
+ private fun IndexFilterChip (
263
+ modifier : Modifier = Modifier ,
264
+ selected : Boolean ,
265
+ onClick : () -> Unit ,
266
+ label : String
267
+ ) {
268
+ FilterChip (
269
+ modifier = modifier,
270
+ selected = selected,
271
+ onClick = onClick
272
+ ) {
273
+ Row (
274
+ horizontalArrangement = Arrangement .spacedBy(4 .dp),
275
+ verticalAlignment = Alignment .CenterVertically
276
+ ) {
277
+ AnimatedVisibility (visible = selected) {
278
+ Icon (
279
+ modifier = Modifier .size(20 .dp),
280
+ imageVector = Icons .Default .Check ,
281
+ contentDescription = null
282
+ )
283
+ }
284
+ Text (text = label)
285
+ }
286
+ }
287
+ }
288
+
289
+ @OptIn(ExperimentalTvMaterial3Api ::class )
290
+ @Composable
291
+ fun <T > IndexFilterChipRow (
292
+ modifier : Modifier = Modifier ,
293
+ title : String ,
294
+ filter : Map <T , String >,
295
+ selectedFilterId : T ,
296
+ onFilterIdChange : (T ) -> Unit
297
+ ) {
298
+ val focusRestorerModifiers = createCustomInitialFocusRestorerModifiers()
299
+
300
+ Row (
301
+ horizontalArrangement = Arrangement .spacedBy(8 .dp),
302
+ verticalAlignment = Alignment .CenterVertically
303
+ ) {
304
+ Text (
305
+ text = title,
306
+ style = MaterialTheme .typography.labelLarge
307
+ )
308
+ TvLazyRow (
309
+ modifier = modifier
310
+ .then(focusRestorerModifiers.parentModifier),
311
+ horizontalArrangement = Arrangement .spacedBy(8 .dp),
312
+ contentPadding = PaddingValues (horizontal = 8 .dp, vertical = 4 .dp)
313
+ ) {
314
+ items(items = filter.entries.toList()) { (id, label) ->
315
+ IndexFilterChip (
316
+ modifier = Modifier
317
+ .ifElse(selectedFilterId == id, focusRestorerModifiers.childModifier),
318
+ selected = selectedFilterId == id,
319
+ onClick = { onFilterIdChange(id) },
320
+ label = label
321
+ )
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ fun IndexOrder.getDisplayName (context : Context ) = when (this ) {
328
+ IndexOrder .UpdateTime -> context.getString(R .string.index_order_update_time)
329
+ IndexOrder .DanmakuCount -> context.getString(R .string.index_order_danmaku_count)
330
+ IndexOrder .PlayCount -> context.getString(R .string.index_order_play_count)
331
+ IndexOrder .FollowCount -> context.getString(R .string.index_order_follow_count)
332
+ IndexOrder .Score -> context.getString(R .string.index_order_score)
333
+ IndexOrder .StartTime -> context.getString(R .string.index_order_start_time)
334
+ IndexOrder .PublishTime -> context.getString(R .string.index_order_publish_time)
335
+ }
0 commit comments