Skip to content

Commit 11a651f

Browse files
committed
新增我的追番
1 parent e7c5410 commit 11a651f

File tree

8 files changed

+376
-4
lines changed

8 files changed

+376
-4
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@
242242
android:exported="false"
243243
android:label="@string/title_mobile_activity_favorite"
244244
android:theme="@style/Theme.BVMobile" />
245+
<activity
246+
android:name=".mobile.activities.FollowingSeasonActivity"
247+
android:exported="false"
248+
android:label="@string/title_mobile_activity_following_season"
249+
android:theme="@style/Theme.BVMobile" />
245250
</application>
246251

247252
</manifest>

app/src/main/kotlin/dev/aaa1115910/bv/entity/carddata/SeasonCardData.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,15 @@ data class SeasonCardData(
2323
badge = null
2424
)
2525
}
26+
27+
fun fromFollowingSeason(followingSeason: dev.aaa1115910.biliapi.entity.season.FollowingSeason): SeasonCardData {
28+
return SeasonCardData(
29+
seasonId = followingSeason.seasonId,
30+
title = followingSeason.title,
31+
cover = followingSeason.cover,
32+
rating = null,
33+
badge = null
34+
)
35+
}
2636
}
2737
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package dev.aaa1115910.bv.mobile.activities
2+
3+
import android.os.Bundle
4+
import androidx.activity.ComponentActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
7+
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
8+
import dev.aaa1115910.bv.mobile.screen.FollowingSeasonScreen
9+
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
10+
11+
class FollowingSeasonActivity : ComponentActivity() {
12+
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
13+
override fun onCreate(savedInstanceState: Bundle?) {
14+
super.onCreate(savedInstanceState)
15+
setContent {
16+
val windowSize = calculateWindowSizeClass(this)
17+
BVMobileTheme {
18+
FollowingSeasonScreen(
19+
windowSize = windowSize
20+
)
21+
}
22+
}
23+
}
24+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package dev.aaa1115910.bv.mobile.component.videocard
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.aspectRatio
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.layout.width
11+
import androidx.compose.foundation.lazy.grid.GridCells
12+
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
13+
import androidx.compose.material3.Card
14+
import androidx.compose.material3.CardDefaults
15+
import androidx.compose.material3.MaterialTheme
16+
import androidx.compose.material3.Text
17+
import androidx.compose.runtime.Composable
18+
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.mutableStateOf
20+
import androidx.compose.runtime.remember
21+
import androidx.compose.runtime.setValue
22+
import androidx.compose.ui.Alignment
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.draw.clip
25+
import androidx.compose.ui.graphics.Brush
26+
import androidx.compose.ui.graphics.Color
27+
import androidx.compose.ui.layout.ContentScale
28+
import androidx.compose.ui.layout.onGloballyPositioned
29+
import androidx.compose.ui.platform.LocalDensity
30+
import androidx.compose.ui.text.font.FontStyle
31+
import androidx.compose.ui.text.font.FontWeight
32+
import androidx.compose.ui.text.style.TextAlign
33+
import androidx.compose.ui.text.style.TextOverflow
34+
import androidx.compose.ui.tooling.preview.Preview
35+
import androidx.compose.ui.unit.Dp
36+
import androidx.compose.ui.unit.dp
37+
import androidx.compose.ui.unit.sp
38+
import coil.compose.AsyncImage
39+
import dev.aaa1115910.bv.entity.carddata.SeasonCardData
40+
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
41+
42+
@Composable
43+
fun SeasonCard(
44+
modifier: Modifier = Modifier,
45+
data: SeasonCardData,
46+
coverHeight: Dp? = null,
47+
onClick: () -> Unit = {},
48+
) {
49+
val localDensity = LocalDensity.current
50+
var coverRealWidth by remember { mutableStateOf(0.dp) }
51+
52+
Card(
53+
modifier = modifier,
54+
onClick = onClick,
55+
shape = MaterialTheme.shapes.large,
56+
colors = CardDefaults.cardColors(
57+
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
58+
)
59+
) {
60+
Column {
61+
val coverModifier = if (coverHeight != null) {
62+
Modifier.height(coverHeight)
63+
} else {
64+
Modifier.fillMaxWidth()
65+
}
66+
val textBoxModifier = if (coverHeight != null) {
67+
Modifier.width((0.75 * coverHeight.value).dp)
68+
} else {
69+
Modifier
70+
}
71+
72+
Box(
73+
modifier = Modifier.clip(MaterialTheme.shapes.large),
74+
contentAlignment = Alignment.BottomCenter
75+
) {
76+
AsyncImage(
77+
modifier = coverModifier
78+
.aspectRatio(0.75f)
79+
.clip(MaterialTheme.shapes.large)
80+
.onGloballyPositioned { coordinates ->
81+
coverRealWidth = with(localDensity) { coordinates.size.width.toDp() }
82+
},
83+
model = data.cover,
84+
contentDescription = null,
85+
contentScale = ContentScale.FillBounds
86+
)
87+
88+
if (data.rating != null) {
89+
Box(
90+
modifier = Modifier
91+
.height(48.dp)
92+
// 无法使用 fillMaxWidth 来确定宽度
93+
.width(coverRealWidth)
94+
.background(
95+
Brush.verticalGradient(
96+
colors = listOf(
97+
Color.Transparent,
98+
Color.Black.copy(alpha = 0.8f)
99+
)
100+
)
101+
)
102+
)
103+
Text(
104+
modifier = Modifier
105+
.align(Alignment.BottomEnd)
106+
.fillMaxWidth()
107+
.padding(8.dp, 0.dp),
108+
text = data.rating,
109+
fontStyle = FontStyle.Italic,
110+
fontWeight = FontWeight.Bold,
111+
fontSize = 24.sp,
112+
textAlign = TextAlign.End,
113+
color = Color.White
114+
)
115+
}
116+
}
117+
118+
Column(
119+
modifier = textBoxModifier.padding(8.dp)
120+
) {
121+
Text(
122+
text = data.title,
123+
style = MaterialTheme.typography.titleMedium,
124+
maxLines = 1,
125+
overflow = TextOverflow.Ellipsis
126+
)
127+
if (data.subTitle != null) {
128+
Text(
129+
text = data.subTitle,
130+
maxLines = 1,
131+
overflow = TextOverflow.Ellipsis,
132+
fontSize = 12.sp,
133+
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
134+
)
135+
}
136+
}
137+
}
138+
}
139+
}
140+
141+
@Preview(device = "id:tv_1080p")
142+
@Composable
143+
private fun SeasonCardPreview() {
144+
BVMobileTheme {
145+
LazyVerticalGrid(columns = GridCells.Fixed(6)) {
146+
repeat(6) {
147+
item {
148+
SeasonCard(
149+
data = SeasonCardData(
150+
seasonId = 40794,
151+
title = "007:没空去死",
152+
cover = "http://i0.hdslb.com/bfs/bangumi/image/8d211c396aad084d6fa413015200dda6ed260768.png",
153+
rating = "8.6"
154+
)
155+
)
156+
}
157+
}
158+
}
159+
}
160+
}
161+
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package dev.aaa1115910.bv.mobile.screen
2+
3+
import android.app.Activity
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.PaddingValues
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.lazy.grid.GridCells
9+
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
10+
import androidx.compose.foundation.lazy.grid.itemsIndexed
11+
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
12+
import androidx.compose.material.icons.Icons
13+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
14+
import androidx.compose.material3.ExperimentalMaterial3Api
15+
import androidx.compose.material3.Icon
16+
import androidx.compose.material3.IconButton
17+
import androidx.compose.material3.LargeTopAppBar
18+
import androidx.compose.material3.PrimaryTabRow
19+
import androidx.compose.material3.Scaffold
20+
import androidx.compose.material3.Tab
21+
import androidx.compose.material3.Text
22+
import androidx.compose.material3.TopAppBarDefaults
23+
import androidx.compose.material3.rememberTopAppBarState
24+
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
25+
import androidx.compose.material3.windowsizeclass.WindowSizeClass
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.mutableStateOf
28+
import androidx.compose.runtime.remember
29+
import androidx.compose.runtime.getValue
30+
import androidx.compose.runtime.setValue
31+
import androidx.compose.ui.Modifier
32+
import androidx.compose.ui.input.nestedscroll.nestedScroll
33+
import androidx.compose.ui.platform.LocalContext
34+
import androidx.compose.ui.tooling.preview.Preview
35+
import androidx.compose.ui.unit.dp
36+
import dev.aaa1115910.biliapi.entity.season.FollowingSeasonType
37+
import dev.aaa1115910.bv.entity.carddata.SeasonCardData
38+
import dev.aaa1115910.bv.mobile.component.videocard.SeasonCard
39+
import dev.aaa1115910.bv.mobile.theme.BVMobileTheme
40+
import dev.aaa1115910.bv.util.OnBottomReached
41+
import dev.aaa1115910.bv.util.calculateWindowSizeClassInPreview
42+
import dev.aaa1115910.bv.util.getDisplayName
43+
import dev.aaa1115910.bv.viewmodel.user.FollowingSeasonViewModel
44+
import org.koin.androidx.compose.koinViewModel
45+
46+
@Composable
47+
fun FollowingSeasonScreen(
48+
modifier: Modifier = Modifier,
49+
windowSize: WindowSizeClass,
50+
followingSeasonViewModel: FollowingSeasonViewModel = koinViewModel()
51+
) {
52+
val context = LocalContext.current
53+
val listState = rememberLazyGridState()
54+
55+
listState.OnBottomReached(
56+
loading = followingSeasonViewModel.updating
57+
) {
58+
if (followingSeasonViewModel.noMore) return@OnBottomReached
59+
followingSeasonViewModel.loadMore()
60+
}
61+
62+
FollowingSeasonContent(
63+
modifier = modifier,
64+
windowSize = windowSize,
65+
type = followingSeasonViewModel.followingSeasonType,
66+
seasons = followingSeasonViewModel.followingSeasons.map(SeasonCardData::fromFollowingSeason),
67+
onBack = { (context as Activity).finish() },
68+
onTypeChange = {
69+
followingSeasonViewModel.followingSeasonType = it
70+
followingSeasonViewModel.clearData()
71+
followingSeasonViewModel.loadMore()
72+
},
73+
onClickSeason = {}
74+
)
75+
}
76+
77+
@OptIn(ExperimentalMaterial3Api::class)
78+
@Composable
79+
private fun FollowingSeasonContent(
80+
modifier: Modifier = Modifier,
81+
windowSize: WindowSizeClass,
82+
type: FollowingSeasonType,
83+
seasons: List<SeasonCardData>,
84+
onBack: () -> Unit,
85+
onTypeChange: (FollowingSeasonType) -> Unit,
86+
onClickSeason: (SeasonCardData) -> Unit,
87+
) {
88+
val context = LocalContext.current
89+
val scrollBehavior =
90+
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
91+
92+
Scaffold(
93+
modifier = modifier
94+
.nestedScroll(scrollBehavior.nestedScrollConnection),
95+
topBar = {
96+
Column {
97+
LargeTopAppBar(
98+
title = { Text(text = "我的追番") },
99+
navigationIcon = {
100+
IconButton(onClick = onBack) {
101+
Icon(
102+
imageVector = Icons.AutoMirrored.Default.ArrowBack,
103+
contentDescription = null
104+
)
105+
}
106+
},
107+
scrollBehavior = scrollBehavior
108+
)
109+
PrimaryTabRow(
110+
selectedTabIndex = type.ordinal,
111+
) {
112+
FollowingSeasonType.entries.forEach { seasonType ->
113+
Tab(
114+
selected = type == seasonType,
115+
text = { Text(text = seasonType.getDisplayName(context)) },
116+
onClick = { onTypeChange(seasonType) }
117+
)
118+
}
119+
}
120+
}
121+
},
122+
) { innerPadding ->
123+
LazyVerticalGrid(
124+
modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),
125+
columns = GridCells.Adaptive(100.dp),
126+
contentPadding = PaddingValues(12.dp),
127+
verticalArrangement = Arrangement.spacedBy(12.dp),
128+
horizontalArrangement = Arrangement.spacedBy(12.dp)
129+
) {
130+
itemsIndexed(seasons) { index, season ->
131+
SeasonCard(
132+
data = season,
133+
onClick = { onClickSeason(season) }
134+
)
135+
}
136+
}
137+
}
138+
}
139+
140+
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
141+
@Preview
142+
@Composable
143+
private fun FollowingSeasonContentPreview() {
144+
val windowSize = calculateWindowSizeClassInPreview()
145+
var selectedType by remember { mutableStateOf(FollowingSeasonType.Bangumi) }
146+
147+
val seasons = (1..50).map {
148+
SeasonCardData(
149+
seasonId = it,
150+
title = "Title $it",
151+
cover = "http://i0.hdslb.com/bfs/bangumi/image/8d211c396aad084d6fa413015200dda6ed260768.png",
152+
rating = "8.6"
153+
)
154+
}
155+
156+
BVMobileTheme {
157+
FollowingSeasonContent(
158+
windowSize = windowSize,
159+
type = selectedType,
160+
seasons = seasons,
161+
onBack = {},
162+
onTypeChange = { selectedType = it },
163+
onClickSeason = {}
164+
)
165+
}
166+
}

0 commit comments

Comments
 (0)