diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/activities/MainActivity.kt b/app/src/main/kotlin/dev/aaa1115910/bv/activities/MainActivity.kt index 003ec93d..a7b7e5c2 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/activities/MainActivity.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/activities/MainActivity.kt @@ -12,7 +12,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import dev.aaa1115910.bv.repository.UserRepository -import dev.aaa1115910.bv.screen.HomeScreen +import dev.aaa1115910.bv.screen.MainScreen import dev.aaa1115910.bv.screen.RegionBlockScreen import dev.aaa1115910.bv.screen.user.lock.UnlockUserScreen import dev.aaa1115910.bv.ui.theme.BVTheme @@ -67,7 +67,7 @@ class MainActivity : ComponentActivity() { } else { //HomeScreen() if (!userLockLocked) { - HomeScreen() + MainScreen() } else { UnlockUserScreen( onUnlockSuccess = { user -> diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/activities/anime/AnimeIndexActivity.kt b/app/src/main/kotlin/dev/aaa1115910/bv/activities/anime/AnimeIndexActivity.kt index c2c995fb..c8d425b9 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/activities/anime/AnimeIndexActivity.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/activities/anime/AnimeIndexActivity.kt @@ -3,7 +3,7 @@ package dev.aaa1115910.bv.activities.anime import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import dev.aaa1115910.bv.screen.home.anime.AnimeIndexScreen +import dev.aaa1115910.bv.screen.main.pgc.anime.AnimeIndexScreen import dev.aaa1115910.bv.ui.theme.BVTheme class AnimeIndexActivity : ComponentActivity() { diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/activities/anime/AnimeTimelineActivity.kt b/app/src/main/kotlin/dev/aaa1115910/bv/activities/anime/AnimeTimelineActivity.kt index 3aa04d79..f2832f76 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/activities/anime/AnimeTimelineActivity.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/activities/anime/AnimeTimelineActivity.kt @@ -3,7 +3,7 @@ package dev.aaa1115910.bv.activities.anime import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import dev.aaa1115910.bv.screen.home.anime.AnimeTimelineScreen +import dev.aaa1115910.bv.screen.main.pgc.anime.AnimeTimelineScreen import dev.aaa1115910.bv.ui.theme.BVTheme class AnimeTimelineActivity : ComponentActivity() { diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/activities/search/SearchInputActivity.kt b/app/src/main/kotlin/dev/aaa1115910/bv/activities/search/SearchInputActivity.kt index 86c27aee..5c093a0d 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/activities/search/SearchInputActivity.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/activities/search/SearchInputActivity.kt @@ -3,6 +3,8 @@ package dev.aaa1115910.bv.activities.search import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.runtime.remember +import androidx.compose.ui.focus.FocusRequester import dev.aaa1115910.bv.screen.search.SearchInputScreen import dev.aaa1115910.bv.ui.theme.BVTheme @@ -10,8 +12,9 @@ class SearchInputActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + val defaultFocusRequester = remember { FocusRequester() } BVTheme { - SearchInputScreen() + SearchInputScreen(defaultFocusRequester = defaultFocusRequester) } } } diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/component/DevelopingTip.kt b/app/src/main/kotlin/dev/aaa1115910/bv/component/DevelopingTip.kt new file mode 100644 index 00000000..fcd1900a --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/component/DevelopingTip.kt @@ -0,0 +1,59 @@ +package dev.aaa1115910.bv.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import dev.aaa1115910.bv.ui.theme.BVTheme + +@Composable +fun DevelopingTip(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Text( + text = "\uD83D\uDEA7", + style = MaterialTheme.typography.displayLarge + ) + Text( + text = "前方施工 请绕行", + style = MaterialTheme.typography.titleLarge + ) + } +} + +@Composable +fun DevelopingTipContent(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + DevelopingTip() + } +} + +@Preview +@Composable +private fun DevelopingTipPreview() { + BVTheme { + DevelopingTip() + } +} + +@Preview(device = "id:tv_1080p") +@Composable +private fun DevelopingTipContentPreview() { + BVTheme { + DevelopingTipContent() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/component/TopNav.kt b/app/src/main/kotlin/dev/aaa1115910/bv/component/TopNav.kt index c8f49d21..ab18d43c 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/component/TopNav.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/component/TopNav.kt @@ -1,170 +1,73 @@ package dev.aaa1115910.bv.component import android.content.Context -import android.content.Intent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material.icons.rounded.Settings import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.tv.foundation.ExperimentalTvFoundationApi -import androidx.tv.material3.Button -import androidx.tv.material3.ButtonDefaults -import androidx.tv.material3.Icon -import androidx.tv.material3.IconButton -import androidx.tv.material3.IconButtonDefaults import androidx.tv.material3.LocalContentColor import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Surface -import androidx.tv.material3.SurfaceDefaults import androidx.tv.material3.Tab import androidx.tv.material3.TabRow import androidx.tv.material3.TabRowScope import androidx.tv.material3.Text -import coil.compose.AsyncImage import dev.aaa1115910.bv.BVApp -import dev.aaa1115910.bv.R -import dev.aaa1115910.bv.activities.settings.SettingsActivity -import dev.aaa1115910.bv.activities.user.LoginActivity -import dev.aaa1115910.bv.activities.user.UserInfoActivity -import dev.aaa1115910.bv.ui.theme.BVTheme -import kotlinx.coroutines.delay -@OptIn(ExperimentalTvFoundationApi::class) @Composable fun TopNav( modifier: Modifier = Modifier, - isLogin: Boolean, - username: String, - face: String, - settingsButtonFocusRequester: FocusRequester, - onSelectedChange: (TopNavItem) -> Unit = {}, - onClick: (TopNavItem) -> Unit = {}, - onShowUserPanel: () -> Unit = {} + items: List, + isLargePadding:Boolean, + onSelectedChanged: (TopNavItem) -> Unit = {}, + onClick: (TopNavItem) -> Unit = {} ) { - val context = LocalContext.current - var selectedNav by remember { mutableStateOf(TopNavItem.Popular) } - val navList = - listOf( - TopNavItem.Search, - TopNavItem.Recommend, - TopNavItem.Popular, - TopNavItem.Anime, - TopNavItem.Dynamics - ) + val focusRestorerModifiers = createCustomInitialFocusRestorerModifiers() - LaunchedEffect(selectedNav) { - delay(250) - onSelectedChange(selectedNav) - } + var selectedNav by remember { mutableStateOf(items.first()) } + var selectedTabIndex by remember { mutableIntStateOf(0) } + val verticalPadding by animateDpAsState( + targetValue = if (isLargePadding) 24.dp else 12.dp, + label = "top nav vertical padding" + ) - Box( + Row( modifier = modifier .fillMaxWidth() - .padding(8.dp) + .padding(12.dp, verticalPadding), + horizontalArrangement = Arrangement.Center ) { - FocusGroup { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "Bug Video", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .alpha(0.5f) - .padding(horizontal = 26.dp) - ) - - var selectedTabIndex by remember { mutableIntStateOf(1) } - - TabRow( - selectedTabIndex = selectedTabIndex, - separator = { Spacer(modifier = Modifier.width(12.dp)) }, - ) { - navList.forEachIndexed { index, tab -> - NavItemTab( - modifier = if (index == 1) Modifier.initiallyFocused() else Modifier.restorableFocus(), - topNavItem = tab, - selected = index == selectedTabIndex, - onFocus = { - if (tab != TopNavItem.Search) selectedNav = tab - selectedTabIndex = index - }, - onClick = { onClick(tab) } - ) - } - } - } - Row( - verticalAlignment = Alignment.CenterVertically - ) { - SettingsIcon( - modifier = Modifier - .restorableFocus() - .focusRequester(settingsButtonFocusRequester), - onClick = { - context.startActivity(Intent(context, SettingsActivity::class.java)) - } - ) - UserIcon( - modifier = Modifier - .restorableFocus() - .padding(end = 12.dp), - isLogin = isLogin, - username = username, - face = face, - onGotoLogin = { - context.startActivity(Intent(context, LoginActivity::class.java)) - }, - onGotoInfo = { - context.startActivity(Intent(context, UserInfoActivity::class.java)) - }, - onFocused = { - if (isLogin) { - onShowUserPanel() - } - } - ) - } + TabRow( + modifier = Modifier + .then(focusRestorerModifiers.parentModifier), + selectedTabIndex = selectedTabIndex, + separator = { Spacer(modifier = Modifier.width(12.dp)) }, + ) { + items.forEachIndexed { index, tab -> + NavItemTab( + modifier = Modifier + .ifElse(index == 0, focusRestorerModifiers.childModifier), + topNavItem = tab, + selected = index == selectedTabIndex, + onFocus = { + selectedNav = tab + selectedTabIndex = index + onSelectedChanged(tab) + }, + onClick = { onClick(tab) } + ) } } } @@ -186,166 +89,63 @@ private fun TabRowScope.NavItemTab( onFocus = onFocus, onClick = onClick ) { - if (topNavItem == TopNavItem.Search) { - Row( - modifier = Modifier - .height(32.dp) - .padding( - horizontal = 16.dp, - vertical = 6.dp - ), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Rounded.Search, - contentDescription = null, - tint = LocalContentColor.current, - ) - AnimatedVisibility(visible = selected) { - Text( - text = topNavItem.getDisplayName(context), - color = LocalContentColor.current, - style = MaterialTheme.typography.labelLarge - - ) - } - } - } else { - Text( - modifier = Modifier - .height(32.dp) - .padding(horizontal = 16.dp, vertical = 6.dp), - text = topNavItem.getDisplayName(context), - color = LocalContentColor.current, - style = MaterialTheme.typography.labelLarge - ) - } + Text( + modifier = Modifier + .height(32.dp) + .padding(horizontal = 16.dp, vertical = 6.dp), + text = topNavItem.getDisplayName(context), + color = LocalContentColor.current, + style = MaterialTheme.typography.labelLarge + ) } } -@Composable -private fun SettingsIcon( - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - var hasFocus by remember { mutableStateOf(false) } - - val infiniteTransition = rememberInfiniteTransition( - label = "settings icon infinite transition" - ) - - val iconRotate by infiniteTransition.animateFloat( - initialValue = 0f, - targetValue = 60f, - animationSpec = infiniteRepeatable( - tween(1000, easing = LinearEasing), RepeatMode.Restart - ), - label = "settings icon rotate" - ) - - IconButton( - modifier = modifier.onFocusChanged { hasFocus = it.hasFocus }, - onClick = onClick, - colors = IconButtonDefaults.colors( - containerColor = Color.Transparent - ) - ) { - Icon( - modifier = Modifier.rotate(if (hasFocus) iconRotate else 0f), - imageVector = Icons.Rounded.Settings, - contentDescription = null - ) - } +interface TopNavItem { + fun getDisplayName(context: Context = BVApp.context): String } -@Composable -private fun UserIcon( - modifier: Modifier = Modifier, - isLogin: Boolean, - username: String, - face: String, - onGotoLogin: () -> Unit, - onGotoInfo: () -> Unit, - onFocused: () -> Unit -) { - var hasFocus by remember { mutableStateOf(false) } - Button( - modifier = modifier - .onFocusChanged { - hasFocus = it.hasFocus - if (it.hasFocus) onFocused() - }, - onClick = { if (isLogin) onGotoInfo() else onGotoLogin() }, - colors = ButtonDefaults.colors( - containerColor = Color.Transparent, - focusedContainerColor = if (isLogin) Color.Transparent else MaterialTheme.colorScheme.onSurface, - focusedContentColor = if (isLogin) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) else MaterialTheme.colorScheme.inverseOnSurface - ) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End) - ) { - Text(text = if (isLogin) username else "未登录") - Box { - Surface( - modifier = Modifier - .size(40.dp) - .clip(CircleShape), - colors = SurfaceDefaults.colors( - containerColor = Color.White - ) - ) { - AsyncImage( - modifier = Modifier - .size(40.dp) - .clip(CircleShape), - model = face, - contentDescription = null, - contentScale = ContentScale.FillBounds - ) - } - } - } +enum class HomeTopNavItem(private val displayName: String) : TopNavItem { + Recommend("推荐"), + Popular("热门"), + Dynamics("动态"); + + override fun getDisplayName(context: Context): String { + return displayName } } -enum class TopNavItem(private val _displayNameResId: Int) { - Search(R.string.top_nav_item_search), - Recommend(R.string.top_nav_item_recommend), - Popular(R.string.top_nav_item_popular), - Partition(R.string.top_nav_item_partition), - Anime(R.string.top_nav_item_anime), - Dynamics(R.string.top_nav_item_dynamics); - - fun getDisplayName(context: Context = BVApp.context): String { - return context.getString(_displayNameResId) +enum class UgcTopNavItem(private val displayName: String) : TopNavItem { + Douga("动画"), + Game("游戏"), + Kichiku("鬼畜"), + Music("音乐"), + Dance("舞蹈"), + Cinephile("影视"), + Ent("娱乐"), + Knowledge("知识"), + Tech("科技"), + Information("资讯"), + Food("美食"), + Life("生活"), + Car("汽车"), + Fashion("时尚"), + Sports("体育"), + Animal("动物圈"); + + override fun getDisplayName(context: Context): String { + return displayName } } -@Preview -@Composable -private fun UserIconPreview() { - var isLogin by remember { mutableStateOf(false) } +enum class PgcTopNavItem(private val displayName: String) : TopNavItem { + Anime("番剧"), + GuoChuang("国创"), + Movie("电影"), + Documentary("纪录片"), + Tv("电视剧"), + Variety("综艺"); - BVTheme { - Surface { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Button(onClick = { isLogin = !isLogin }) { - Text("is login: $isLogin") - } - UserIcon( - modifier = Modifier.padding(4.dp), - isLogin = isLogin, - username = "bishi", - face = "", - onGotoLogin = {}, - onGotoInfo = {}, - onFocused = {} - ) - } - } + override fun getDisplayName(context: Context): String { + return displayName } } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/component/UserPanel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/component/UserPanel.kt index b1b0a3f3..7eef8387 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/component/UserPanel.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/component/UserPanel.kt @@ -148,7 +148,7 @@ fun UserPanel( .onPreviewKeyEvent { println(it.nativeKeyEvent) when (it.nativeKeyEvent.keyCode) { - KeyEvent.KEYCODE_DPAD_LEFT -> { + KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_DOWN -> { if (it.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) onHide() return@onPreviewKeyEvent true } @@ -165,7 +165,17 @@ fun UserPanel( } item { UserPanelSmallItem( - modifier = Modifier, + modifier = Modifier + .onPreviewKeyEvent { + println(it.nativeKeyEvent) + when (it.nativeKeyEvent.keyCode) { + KeyEvent.KEYCODE_DPAD_DOWN -> { + if (it.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) onHide() + return@onPreviewKeyEvent true + } + } + false + }, title = "现在不看", icon = Icons.Rounded.Schedule, onClick = { diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/HomeScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/HomeScreen.kt deleted file mode 100644 index bc2baabc..00000000 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/HomeScreen.kt +++ /dev/null @@ -1,309 +0,0 @@ -package dev.aaa1115910.bv.screen - -import android.app.Activity -import android.content.Intent -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.shrinkHorizontally -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import dev.aaa1115910.bv.R -import dev.aaa1115910.bv.activities.search.SearchInputActivity -import dev.aaa1115910.bv.activities.user.FavoriteActivity -import dev.aaa1115910.bv.activities.user.FollowingSeasonActivity -import dev.aaa1115910.bv.activities.user.HistoryActivity -import dev.aaa1115910.bv.activities.user.UserInfoActivity -import dev.aaa1115910.bv.component.TopNav -import dev.aaa1115910.bv.component.TopNavItem -import dev.aaa1115910.bv.component.UserPanel -import dev.aaa1115910.bv.screen.home.AnimeScreen -import dev.aaa1115910.bv.screen.home.DynamicsScreen -import dev.aaa1115910.bv.screen.home.PartitionScreen -import dev.aaa1115910.bv.screen.home.PopularScreen -import dev.aaa1115910.bv.screen.home.RecommendScreen -import dev.aaa1115910.bv.util.fInfo -import dev.aaa1115910.bv.util.requestFocus -import dev.aaa1115910.bv.util.toast -import dev.aaa1115910.bv.viewmodel.UserViewModel -import dev.aaa1115910.bv.viewmodel.home.DynamicViewModel -import dev.aaa1115910.bv.viewmodel.home.PopularViewModel -import dev.aaa1115910.bv.viewmodel.home.RecommendViewModel -import io.github.oshai.kotlinlogging.KotlinLogging -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.koin.androidx.compose.koinViewModel - -@Composable -fun HomeScreen( - modifier: Modifier = Modifier, - recommendViewModel: RecommendViewModel = koinViewModel(), - popularViewModel: PopularViewModel = koinViewModel(), - dynamicViewModel: DynamicViewModel = koinViewModel(), - userViewModel: UserViewModel = koinViewModel() -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val logger = KotlinLogging.logger { } - - val recommendState = rememberLazyGridState() - val popularState = rememberLazyGridState() - val animeState = rememberLazyListState() - val dynamicState = rememberLazyGridState() - - var selectedTab by remember { mutableStateOf(TopNavItem.Popular) } - var showUserPanel by remember { mutableStateOf(false) } - var lastPressBack: Long by remember { mutableLongStateOf(0L) } - - val settingsButtonFocusRequester = remember { FocusRequester() } - val navFocusRequester = remember { FocusRequester() } - - val onFocusBackToNav: () -> Unit = { - logger.fInfo { "onFocusBackToNav" } - navFocusRequester.requestFocus(scope) - } - - //启动时刷新数据 - LaunchedEffect(Unit) { - navFocusRequester.requestFocus() - scope.launch(Dispatchers.IO) { - recommendViewModel.loadMore() - } - scope.launch(Dispatchers.IO) { - popularViewModel.loadMore() - } - scope.launch(Dispatchers.IO) { - dynamicViewModel.loadMore() - } - scope.launch(Dispatchers.IO) { - userViewModel.updateUserInfo() - } - } - - //监听登录变化 - LaunchedEffect(userViewModel.isLogin) { - if (userViewModel.isLogin) { - //login - userViewModel.updateUserInfo() - } else { - //logout - userViewModel.clearUserInfo() - } - } - - val handleBack = { - val currentTime = System.currentTimeMillis() - if (currentTime - lastPressBack < 1000 * 3) { - logger.fInfo { "Exiting bug video" } - (context as Activity).finish() - } else { - lastPressBack = currentTime - R.string.home_press_back_again_to_exit.toast(context) - } - } - - BackHandler(!showUserPanel) { - handleBack() - } - - Box( - modifier = modifier - ) { - Scaffold( - modifier = Modifier, - topBar = { - TopNav( - modifier = Modifier.focusRequester(navFocusRequester), - isLogin = userViewModel.isLogin, - username = userViewModel.username, - face = userViewModel.face, - settingsButtonFocusRequester = settingsButtonFocusRequester, - onSelectedChange = { nav -> - selectedTab = nav - when (nav) { - TopNavItem.Recommend -> { - - } - - TopNavItem.Popular -> { - //scope.launch(Dispatchers.Default) { popularState.scrollToItem(0, 0) } - } - - TopNavItem.Partition -> { - - } - - TopNavItem.Anime -> { - - } - - TopNavItem.Dynamics -> { - //scope.launch(Dispatchers.Default) { dynamicState.scrollToItem(0, 0) } - if (!dynamicViewModel.loading && dynamicViewModel.isLogin && dynamicViewModel.dynamicList.isEmpty()) { - scope.launch(Dispatchers.Default) { dynamicViewModel.loadMore() } - } - } - - TopNavItem.Search -> { - - } - } - }, - onClick = { nav -> - when (nav) { - TopNavItem.Recommend -> { - logger.fInfo { "clear recommend data" } - recommendViewModel.clearData() - logger.fInfo { "reload recommend data" } - scope.launch(Dispatchers.IO) { recommendViewModel.loadMore() } - } - - TopNavItem.Popular -> { - //scope.launch(Dispatchers.Default) { popularState.scrollToItem(0, 0) } - logger.fInfo { "clear popular data" } - popularViewModel.clearData() - logger.fInfo { "reload popular data" } - scope.launch(Dispatchers.IO) { popularViewModel.loadMore() } - } - - TopNavItem.Partition -> { - - } - - TopNavItem.Anime -> { - - } - - TopNavItem.Dynamics -> { - //scope.launch(Dispatchers.Default) { dynamicState.scrollToItem(0, 0) } - dynamicViewModel.clearData() - scope.launch(Dispatchers.IO) { dynamicViewModel.loadMore() } - } - - TopNavItem.Search -> { - context.startActivity( - Intent(context, SearchInputActivity::class.java) - ) - } - } - }, - onShowUserPanel = { showUserPanel = true } - ) - } - ) { innerPadding -> - Box( - modifier = Modifier.padding(innerPadding) - ) { - Crossfade( - targetState = selectedTab, - label = "home content cross fade" - ) { screen -> - when (screen) { - TopNavItem.Recommend -> RecommendScreen( - lazyGridState = recommendState, - onBackNav = onFocusBackToNav - ) - - TopNavItem.Popular -> PopularScreen( - lazyGridState = popularState, - onBackNav = onFocusBackToNav - ) - - TopNavItem.Partition -> PartitionScreen() - TopNavItem.Anime -> AnimeScreen( - lazyListState = animeState, - onBackNav = onFocusBackToNav - ) - - TopNavItem.Dynamics -> DynamicsScreen( - lazyGridState = dynamicState, - onBackNav = onFocusBackToNav - ) - - else -> PopularScreen( - lazyGridState = popularState, - onBackNav = onFocusBackToNav - ) - } - } - } - } - - AnimatedVisibility( - visible = showUserPanel, - enter = fadeIn(), - exit = fadeOut() - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.4f)) - ) { - AnimatedVisibility( - modifier = Modifier - .align(Alignment.TopEnd), - visible = showUserPanel, - enter = fadeIn() + scaleIn(), - exit = shrinkHorizontally() - ) { - UserPanel( - modifier = Modifier - .padding(12.dp) - .onFocusChanged { - if (!it.hasFocus) { - settingsButtonFocusRequester.requestFocus() - } - }, - username = userViewModel.username, - face = userViewModel.face, - onHide = { showUserPanel = false }, - onGoMy = { - context.startActivity(Intent(context, UserInfoActivity::class.java)) - }, - onGoHistory = { - context.startActivity(Intent(context, HistoryActivity::class.java)) - }, - onGoFavorite = { - context.startActivity(Intent(context, FavoriteActivity::class.java)) - }, - onGoFollowing = { - context.startActivity( - Intent( - context, - FollowingSeasonActivity::class.java - ) - ) - }, - onGoLater = { - "按钮放在这只是拿来当摆设的!".toast(context) - } - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/MainScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/MainScreen.kt new file mode 100644 index 00000000..82db186d --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/MainScreen.kt @@ -0,0 +1,217 @@ +package dev.aaa1115910.bv.screen + +import android.app.Activity +import android.content.Intent +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.tv.material3.DrawerValue +import androidx.tv.material3.NavigationDrawer +import androidx.tv.material3.rememberDrawerState +import dev.aaa1115910.bv.R +import dev.aaa1115910.bv.activities.settings.SettingsActivity +import dev.aaa1115910.bv.activities.user.FavoriteActivity +import dev.aaa1115910.bv.activities.user.FollowingSeasonActivity +import dev.aaa1115910.bv.activities.user.HistoryActivity +import dev.aaa1115910.bv.activities.user.LoginActivity +import dev.aaa1115910.bv.activities.user.UserInfoActivity +import dev.aaa1115910.bv.component.UserPanel +import dev.aaa1115910.bv.screen.main.DrawerContent +import dev.aaa1115910.bv.screen.main.DrawerItem +import dev.aaa1115910.bv.screen.main.HomeContent +import dev.aaa1115910.bv.screen.main.PgcContent +import dev.aaa1115910.bv.screen.main.UgcContent +import dev.aaa1115910.bv.screen.search.SearchInputScreen +import dev.aaa1115910.bv.util.fException +import dev.aaa1115910.bv.util.fInfo +import dev.aaa1115910.bv.util.toast +import dev.aaa1115910.bv.viewmodel.UserViewModel +import dev.aaa1115910.bv.viewmodel.home.DynamicViewModel +import dev.aaa1115910.bv.viewmodel.home.PopularViewModel +import dev.aaa1115910.bv.viewmodel.home.RecommendViewModel +import io.github.oshai.kotlinlogging.KotlinLogging +import org.koin.androidx.compose.koinViewModel + +@Composable +fun MainScreen( + modifier: Modifier = Modifier, + recommendViewModel: RecommendViewModel = koinViewModel(), + popularViewModel: PopularViewModel = koinViewModel(), + dynamicViewModel: DynamicViewModel = koinViewModel(), + userViewModel: UserViewModel = koinViewModel() +) { + val context = LocalContext.current + val logger = KotlinLogging.logger("MainScreen") + var showUserPanel by remember { mutableStateOf(false) } + var lastPressBack: Long by remember { mutableLongStateOf(0L) } + var selectedDrawerItem by remember { mutableStateOf(DrawerItem.Home) } + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + + val mainFocusRequester = remember { FocusRequester() } + val ugcFocusRequester = remember { FocusRequester() } + val pgcFocusRequester = remember { FocusRequester() } + val searchFocusRequester = remember { FocusRequester() } + + val handleBack = { + val currentTime = System.currentTimeMillis() + if (currentTime - lastPressBack < 1000 * 3) { + logger.fInfo { "Exiting bug video" } + (context as Activity).finish() + } else { + lastPressBack = currentTime + R.string.home_press_back_again_to_exit.toast(context) + } + } + + val onFocusToContent = { + when (selectedDrawerItem) { + DrawerItem.Home -> mainFocusRequester.requestFocus() + DrawerItem.UGC -> ugcFocusRequester.requestFocus() + DrawerItem.PGC -> pgcFocusRequester.requestFocus() + DrawerItem.Search -> searchFocusRequester.requestFocus() + else -> {} + } + } + + LaunchedEffect(Unit) { + runCatching { + mainFocusRequester.requestFocus() + }.onFailure { + logger.fException(it) { "request default focus requester failed" } + } + } + + BackHandler { + handleBack() + } + + NavigationDrawer( + modifier = modifier, + drawerContent = { + DrawerContent( + isLogin = userViewModel.isLogin, + avatar = userViewModel.face, + username = userViewModel.username, + //avatar = "https://i2.hdslb.com/bfs/face/ef0457addb24141e15dfac6fbf45293ccf1e32ab.jpg", + //username = "碧诗", + onDrawerItemChanged = { selectedDrawerItem = it }, + onOpenSettings = { + context.startActivity(Intent(context, SettingsActivity::class.java)) + }, + onShowUserPanel = { + showUserPanel = true + }, + onFocusToContent = onFocusToContent, + onLogin = { + context.startActivity(Intent(context, LoginActivity::class.java)) + } + ) + }, + drawerState = drawerState + ) { + Box( + modifier = Modifier + ) { + AnimatedContent( + targetState = selectedDrawerItem, + label = "main animated content", + transitionSpec = { + val coefficient = 20 + if (targetState.ordinal < initialState.ordinal) { + fadeIn() + slideInVertically { -it / coefficient } togetherWith + fadeOut() + slideOutVertically { it / coefficient } + } else { + fadeIn() + slideInVertically { it / coefficient } togetherWith + fadeOut() + slideOutVertically { -it / coefficient } + } + } + ) { screen -> + when (screen) { + DrawerItem.Home -> HomeContent(navFocusRequester = mainFocusRequester) + DrawerItem.UGC -> UgcContent(navFocusRequester = ugcFocusRequester) + DrawerItem.PGC -> PgcContent(navFocusRequester = pgcFocusRequester) + DrawerItem.Search -> SearchInputScreen(defaultFocusRequester = searchFocusRequester) + else -> {} + } + } + + AnimatedVisibility( + visible = showUserPanel, + enter = fadeIn(), + exit = fadeOut() + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.4f)) + ) { + AnimatedVisibility( + modifier = Modifier + .align(Alignment.TopEnd), + visible = showUserPanel, + enter = fadeIn() + scaleIn(), + exit = shrinkHorizontally() + ) { + UserPanel( + modifier = Modifier + .padding(12.dp) + .onFocusChanged { + if (!it.hasFocus) { + //settingsButtonFocusRequester.requestFocus() + } + }, + username = userViewModel.username, + face = userViewModel.face, + onHide = { showUserPanel = false }, + onGoMy = { + context.startActivity(Intent(context, UserInfoActivity::class.java)) + }, + onGoHistory = { + context.startActivity(Intent(context, HistoryActivity::class.java)) + }, + onGoFavorite = { + context.startActivity(Intent(context, FavoriteActivity::class.java)) + }, + onGoFollowing = { + context.startActivity( + Intent( + context, + FollowingSeasonActivity::class.java + ) + ) + }, + onGoLater = { + "按钮放在这只是拿来当摆设的!".toast(context) + } + ) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/DrawerContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/DrawerContent.kt new file mode 100644 index 00000000..cf6bc727 --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/DrawerContent.kt @@ -0,0 +1,216 @@ +package dev.aaa1115910.bv.screen.main + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Movie +import androidx.compose.material.icons.filled.OndemandVideo +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.tv.material3.DrawerValue +import androidx.tv.material3.Icon +import androidx.tv.material3.NavigationDrawer +import androidx.tv.material3.NavigationDrawerItem +import androidx.tv.material3.NavigationDrawerScope +import androidx.tv.material3.Surface +import androidx.tv.material3.SurfaceDefaults +import androidx.tv.material3.Text +import androidx.tv.material3.rememberDrawerState +import coil.compose.AsyncImage +import dev.aaa1115910.bv.component.createCustomInitialFocusRestorerModifiers +import dev.aaa1115910.bv.component.ifElse +import dev.aaa1115910.bv.ui.theme.BVTheme +import dev.aaa1115910.bv.util.isDpadRight +import dev.aaa1115910.bv.util.isKeyDown + +@Composable +fun NavigationDrawerScope.DrawerContent( + modifier: Modifier = Modifier, + isLogin: Boolean = false, + avatar: String = "", + username: String = "", + onDrawerItemChanged: (DrawerItem) -> Unit = {}, + onOpenSettings: () -> Unit = {}, + onShowUserPanel: () -> Unit = {}, + onFocusToContent: () -> Unit = {}, + onLogin: () -> Unit = {} +) { + var selectedItem by remember { mutableStateOf(DrawerItem.Home) } + val focusRestorerModifiers = createCustomInitialFocusRestorerModifiers() + + LaunchedEffect(selectedItem) { + onDrawerItemChanged(selectedItem) + } + + Column( + modifier = modifier + .fillMaxHeight() + .padding(12.dp) + .onPreviewKeyEvent { keyEvent -> + if (keyEvent.isDpadRight()) { + if (keyEvent.isKeyDown()) { + onFocusToContent() + return@onPreviewKeyEvent true + } + } + false + }, + verticalArrangement = Arrangement.SpaceBetween + ) { + + NavigationDrawerItem( + modifier = Modifier, + onClick = { + if (isLogin) { + onShowUserPanel() + } else { + onLogin() + } + }, + selected = selectedItem == DrawerItem.User, + leadingContent = { + if (isLogin) { + Surface( + modifier = Modifier + .size(40.dp) + .clip(CircleShape), + colors = SurfaceDefaults.colors( + containerColor = Color.Gray + ) + ) { + AsyncImage( + modifier = Modifier + .size(40.dp) + .clip(CircleShape), + model = avatar, + contentDescription = null, + contentScale = ContentScale.FillBounds + ) + } + } else { + Icon( + imageVector = DrawerItem.User.displayIcon, + contentDescription = null + ) + } + } + ) { + Text( + modifier = Modifier + .basicMarquee(), + text = if (isLogin) username + else DrawerItem.User.displayName, + maxLines = 1 + ) + } + LazyColumn( + modifier = Modifier + .then(focusRestorerModifiers.parentModifier), + verticalArrangement = Arrangement.Center + ) { + listOf( + DrawerItem.Search, + DrawerItem.Home, + DrawerItem.UGC, + DrawerItem.PGC, + ).forEach { item -> + item { + NavigationDrawerItem( + modifier = Modifier + .onFocusChanged { if (it.hasFocus) selectedItem = item } + .ifElse( + item == DrawerItem.Home, + focusRestorerModifiers.childModifier + ), + onClick = { selectedItem = item }, + selected = selectedItem == item, + leadingContent = { + Icon( + imageVector = item.displayIcon, + contentDescription = null + ) + } + ) { + Text(text = item.displayName) + } + } + } + } + NavigationDrawerItem( + modifier = Modifier, + onClick = onOpenSettings, + selected = false, + leadingContent = { + Icon( + imageVector = DrawerItem.Settings.displayIcon, + contentDescription = null + ) + } + ) { + Text(text = DrawerItem.Settings.displayName) + } + } +} + +enum class DrawerItem( + val displayName: String, + val displayIcon: ImageVector +) { + User(displayName = "点击登录", displayIcon = Icons.Default.AccountCircle), + Search(displayName = "搜索", displayIcon = Icons.Default.Search), + Home(displayName = "首页", displayIcon = Icons.Default.Home), + UGC(displayName = "UGC", displayIcon = Icons.Default.OndemandVideo), + PGC(displayName = "PGC", displayIcon = Icons.Default.Movie), + Settings(displayName = "设置", displayIcon = Icons.Default.Settings), ; +} + +@Preview(device = "id:tv_1080p") +@Composable +private fun DrawerContentClosedPreview() { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + BVTheme { + NavigationDrawer( + drawerContent = { + DrawerContent() + }, + drawerState = drawerState + ) { } + } +} + +@Preview(device = "id:tv_1080p") +@Composable +private fun DrawerContentOpenPreview() { + val drawerState = rememberDrawerState(initialValue = DrawerValue.Open) + BVTheme { + NavigationDrawer( + drawerContent = { + DrawerContent() + }, + drawerState = drawerState + ) { } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/HomeContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/HomeContent.kt new file mode 100644 index 00000000..5e2e5545 --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/HomeContent.kt @@ -0,0 +1,199 @@ +package dev.aaa1115910.bv.screen.main + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.rememberLazyGridState +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import dev.aaa1115910.bv.component.HomeTopNavItem +import dev.aaa1115910.bv.component.TopNav +import dev.aaa1115910.bv.screen.main.home.DynamicsScreen +import dev.aaa1115910.bv.screen.main.home.PopularScreen +import dev.aaa1115910.bv.screen.main.home.RecommendScreen +import dev.aaa1115910.bv.util.fInfo +import dev.aaa1115910.bv.util.requestFocus +import dev.aaa1115910.bv.viewmodel.UserViewModel +import dev.aaa1115910.bv.viewmodel.home.DynamicViewModel +import dev.aaa1115910.bv.viewmodel.home.PopularViewModel +import dev.aaa1115910.bv.viewmodel.home.RecommendViewModel +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel + +@Composable +fun HomeContent( + modifier: Modifier = Modifier, + navFocusRequester: FocusRequester, + recommendViewModel: RecommendViewModel = koinViewModel(), + popularViewModel: PopularViewModel = koinViewModel(), + dynamicViewModel: DynamicViewModel = koinViewModel(), + userViewModel: UserViewModel = koinViewModel() +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val logger = KotlinLogging.logger("HomeContent") + + val recommendState = rememberLazyGridState() + val popularState = rememberLazyGridState() + val dynamicState = rememberLazyGridState() + + var selectedTab by remember { mutableStateOf(HomeTopNavItem.Recommend) } + var focusOnContent by remember { mutableStateOf(false) } + var hasFocus by remember { mutableStateOf(false) } + val currentListOnTop by remember { + derivedStateOf { + with( + when (selectedTab) { + HomeTopNavItem.Recommend -> recommendState + HomeTopNavItem.Popular -> popularState + HomeTopNavItem.Dynamics -> dynamicState + } + ) { + firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0 + } + } + } + + //启动时刷新数据 + LaunchedEffect(Unit) { + scope.launch(Dispatchers.IO) { + recommendViewModel.loadMore() + } + scope.launch(Dispatchers.IO) { + popularViewModel.loadMore() + } + scope.launch(Dispatchers.IO) { + dynamicViewModel.loadMore() + } + scope.launch(Dispatchers.IO) { + userViewModel.updateUserInfo() + } + } + + //监听登录变化 + LaunchedEffect(userViewModel.isLogin) { + if (userViewModel.isLogin) { + //login + userViewModel.updateUserInfo() + } else { + //logout + userViewModel.clearUserInfo() + } + } + + LaunchedEffect(hasFocus) { + if (hasFocus) { + navFocusRequester.requestFocus() + } + } + + BackHandler(focusOnContent) { + logger.fInfo { "onFocusBackToNav" } + navFocusRequester.requestFocus(scope) + // scroll to top + scope.launch(Dispatchers.Main) { + when (selectedTab) { + HomeTopNavItem.Recommend -> recommendState.animateScrollToItem(0) + HomeTopNavItem.Popular -> popularState.animateScrollToItem(0) + HomeTopNavItem.Dynamics -> dynamicState.animateScrollToItem(0) + } + } + } + + Scaffold( + modifier = Modifier + .onFocusChanged { hasFocus = it.hasFocus }, + topBar = { + TopNav( + modifier = Modifier + .focusRequester(navFocusRequester) + .focusRequester(navFocusRequester) + .padding(end = 80.dp), + items = HomeTopNavItem.entries, + isLargePadding = !focusOnContent && currentListOnTop, + onSelectedChanged = { nav -> + selectedTab = nav as HomeTopNavItem + when (nav) { + HomeTopNavItem.Recommend -> {} + HomeTopNavItem.Popular -> {} + HomeTopNavItem.Dynamics -> { + if (!dynamicViewModel.loading && dynamicViewModel.isLogin && dynamicViewModel.dynamicList.isEmpty()) { + scope.launch(Dispatchers.IO) { dynamicViewModel.loadMore() } + } + } + } + }, + onClick = { nav -> + when (nav) { + HomeTopNavItem.Recommend -> { + logger.fInfo { "clear recommend data" } + recommendViewModel.clearData() + logger.fInfo { "reload recommend data" } + scope.launch(Dispatchers.IO) { recommendViewModel.loadMore() } + } + + HomeTopNavItem.Popular -> { + logger.fInfo { "clear popular data" } + popularViewModel.clearData() + logger.fInfo { "reload popular data" } + scope.launch(Dispatchers.IO) { popularViewModel.loadMore() } + } + + HomeTopNavItem.Dynamics -> { + dynamicViewModel.clearData() + scope.launch(Dispatchers.IO) { dynamicViewModel.loadMore() } + } + } + } + ) + } + ) { innerPadding -> + Box( + modifier = Modifier + .padding(innerPadding) + .onFocusChanged { focusOnContent = it.hasFocus } + ) { + AnimatedContent( + targetState = selectedTab, + label = "home animated content", + transitionSpec = { + val coefficient = 10 + if (targetState.ordinal < initialState.ordinal) { + fadeIn() + slideInHorizontally { -it / coefficient } togetherWith + fadeOut() + slideOutHorizontally { it / coefficient } + } else { + fadeIn() + slideInHorizontally { it / coefficient } togetherWith + fadeOut() + slideOutHorizontally { -it / coefficient } + } + } + ) { screen -> + when (screen) { + HomeTopNavItem.Recommend -> RecommendScreen(lazyGridState = recommendState) + HomeTopNavItem.Popular -> PopularScreen(lazyGridState = popularState) + HomeTopNavItem.Dynamics -> DynamicsScreen(lazyGridState = dynamicState) + } + } + } + } +} diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/PartitionScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PartitionScreen.kt similarity index 79% rename from app/src/main/kotlin/dev/aaa1115910/bv/screen/home/PartitionScreen.kt rename to app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PartitionScreen.kt index f70ae1e8..63d8cdd7 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/PartitionScreen.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PartitionScreen.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.bv.screen.home +package dev.aaa1115910.bv.screen.main import androidx.compose.runtime.Composable import androidx.tv.material3.Text diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PgcContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PgcContent.kt new file mode 100644 index 00000000..9603c2ec --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/PgcContent.kt @@ -0,0 +1,155 @@ +package dev.aaa1115910.bv.screen.main + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.unit.dp +import dev.aaa1115910.bv.component.DevelopingTipContent +import dev.aaa1115910.bv.component.HomeTopNavItem +import dev.aaa1115910.bv.component.PgcTopNavItem +import dev.aaa1115910.bv.component.TopNav +import dev.aaa1115910.bv.screen.main.pgc.AnimeContent +import dev.aaa1115910.bv.util.fInfo +import dev.aaa1115910.bv.util.requestFocus +import dev.aaa1115910.bv.viewmodel.home.AnimeViewModel +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel + +@Composable +fun PgcContent( + modifier: Modifier = Modifier, + navFocusRequester: FocusRequester, + animeViewModel: AnimeViewModel = koinViewModel() +) { + val scope = rememberCoroutineScope() + val logger = KotlinLogging.logger("PgcContent") + + val animeState = rememberLazyListState() + val guoChuangState = rememberLazyListState() + val movieState = rememberLazyListState() + val documentaryState = rememberLazyListState() + val tvState = rememberLazyListState() + val varietyState = rememberLazyListState() + + var selectedTab by remember { mutableStateOf(PgcTopNavItem.Anime) } + var focusOnContent by remember { mutableStateOf(false) } + val currentListOnTop by remember { + derivedStateOf { + with( + when (selectedTab) { + PgcTopNavItem.Anime -> animeState + PgcTopNavItem.GuoChuang -> guoChuangState + PgcTopNavItem.Movie -> movieState + PgcTopNavItem.Documentary -> documentaryState + PgcTopNavItem.Tv -> tvState + PgcTopNavItem.Variety -> varietyState + } + ) { + firstVisibleItemIndex == 0 && firstVisibleItemScrollOffset == 0 + } + } + } + + //启动时刷新数据 + LaunchedEffect(Unit) { + + } + + BackHandler(focusOnContent) { + logger.fInfo { "onFocusBackToNav" } + navFocusRequester.requestFocus(scope) + // scroll to top + scope.launch(Dispatchers.Main) { + when (selectedTab) { + PgcTopNavItem.Anime -> animeState.animateScrollToItem(0) + PgcTopNavItem.GuoChuang -> guoChuangState.animateScrollToItem(0) + PgcTopNavItem.Movie -> movieState.animateScrollToItem(0) + PgcTopNavItem.Documentary -> documentaryState.animateScrollToItem(0) + PgcTopNavItem.Tv -> tvState.animateScrollToItem(0) + PgcTopNavItem.Variety -> varietyState.animateScrollToItem(0) + } + } + } + + Scaffold( + modifier = Modifier, + topBar = { + TopNav( + modifier = Modifier + .focusRequester(navFocusRequester) + .padding(end = 80.dp), + items = PgcTopNavItem.entries, + isLargePadding = !focusOnContent && currentListOnTop, + onSelectedChanged = { nav -> + selectedTab = nav as PgcTopNavItem + }, + onClick = { nav -> + when (nav) { + PgcTopNavItem.Anime -> { + logger.fInfo { "reload anime data" } + animeViewModel.reloadAll() + } + + PgcTopNavItem.GuoChuang -> {} + PgcTopNavItem.Movie -> {} + PgcTopNavItem.Documentary -> {} + PgcTopNavItem.Tv -> {} + PgcTopNavItem.Variety -> {} + } + } + ) + } + ) { innerPadding -> + Box( + modifier = Modifier + .padding(innerPadding) + .onFocusChanged { focusOnContent = it.hasFocus } + ) { + AnimatedContent( + targetState = selectedTab, + label = "pgc animated content", + transitionSpec = { + val coefficient = 10 + if (targetState.ordinal < initialState.ordinal) { + fadeIn() + slideInHorizontally { -it / coefficient } togetherWith + fadeOut() + slideOutHorizontally { it / coefficient } + } else { + fadeIn() + slideInHorizontally { it / coefficient } togetherWith + fadeOut() + slideOutHorizontally { -it / coefficient } + } + } + ) { screen -> + when (screen) { + PgcTopNavItem.Anime -> AnimeContent(lazyListState = animeState) + PgcTopNavItem.GuoChuang -> DevelopingTipContent() + PgcTopNavItem.Movie -> DevelopingTipContent() + PgcTopNavItem.Documentary -> DevelopingTipContent() + PgcTopNavItem.Tv -> DevelopingTipContent() + PgcTopNavItem.Variety -> DevelopingTipContent() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/UgcContent.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/UgcContent.kt new file mode 100644 index 00000000..de15afc6 --- /dev/null +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/UgcContent.kt @@ -0,0 +1,167 @@ +package dev.aaa1115910.bv.screen.main + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import dev.aaa1115910.bv.component.DevelopingTipContent +import dev.aaa1115910.bv.component.TopNav +import dev.aaa1115910.bv.component.UgcTopNavItem +import dev.aaa1115910.bv.util.fInfo +import dev.aaa1115910.bv.util.requestFocus +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun UgcContent( + modifier: Modifier = Modifier, + navFocusRequester: FocusRequester, +) { + val scope = rememberCoroutineScope() + val logger = KotlinLogging.logger("UgcContent") + + val dougaState = rememberLazyListState() + val gameState = rememberLazyListState() + val kichikuState = rememberLazyListState() + val musicState = rememberLazyListState() + val danceState = rememberLazyListState() + val cinephileState = rememberLazyListState() + val entState = rememberLazyListState() + val knowledgeState = rememberLazyListState() + val techState = rememberLazyListState() + val informationState = rememberLazyListState() + val foodState = rememberLazyListState() + val lifeState = rememberLazyListState() + val carState = rememberLazyListState() + val fashionState = rememberLazyListState() + val sportsState = rememberLazyListState() + val animalState = rememberLazyListState() + + var selectedTab by remember { mutableStateOf(UgcTopNavItem.Douga) } + var focusOnContent by remember { mutableStateOf(false) } + + //启动时刷新数据 + LaunchedEffect(Unit) { + + } + + BackHandler(focusOnContent) { + logger.fInfo { "onFocusBackToNav" } + navFocusRequester.requestFocus(scope) + // scroll to top + scope.launch(Dispatchers.Main) { + when (selectedTab) { + UgcTopNavItem.Douga -> dougaState.animateScrollToItem(0) + UgcTopNavItem.Game -> gameState.animateScrollToItem(0) + UgcTopNavItem.Kichiku -> kichikuState.animateScrollToItem(0) + UgcTopNavItem.Music -> musicState.animateScrollToItem(0) + UgcTopNavItem.Dance -> danceState.animateScrollToItem(0) + UgcTopNavItem.Cinephile -> cinephileState.animateScrollToItem(0) + UgcTopNavItem.Ent -> entState.animateScrollToItem(0) + UgcTopNavItem.Knowledge -> knowledgeState.animateScrollToItem(0) + UgcTopNavItem.Tech -> techState.animateScrollToItem(0) + UgcTopNavItem.Information -> informationState.animateScrollToItem(0) + UgcTopNavItem.Food -> foodState.animateScrollToItem(0) + UgcTopNavItem.Life -> lifeState.animateScrollToItem(0) + UgcTopNavItem.Car -> carState.animateScrollToItem(0) + UgcTopNavItem.Fashion -> fashionState.animateScrollToItem(0) + UgcTopNavItem.Sports -> sportsState.animateScrollToItem(0) + UgcTopNavItem.Animal -> animalState.animateScrollToItem(0) + } + } + } + + Scaffold( + modifier = Modifier, + topBar = { + TopNav( + modifier = Modifier + .focusRequester(navFocusRequester), + items = UgcTopNavItem.entries, + isLargePadding = !focusOnContent, + onSelectedChanged = { nav -> + selectedTab = nav as UgcTopNavItem + }, + onClick = { nav -> + when (nav) { + UgcTopNavItem.Douga -> {} + UgcTopNavItem.Game -> {} + UgcTopNavItem.Kichiku -> {} + UgcTopNavItem.Music -> {} + UgcTopNavItem.Dance -> {} + UgcTopNavItem.Cinephile -> {} + UgcTopNavItem.Ent -> {} + UgcTopNavItem.Knowledge -> {} + UgcTopNavItem.Tech -> {} + UgcTopNavItem.Information -> {} + UgcTopNavItem.Food -> {} + UgcTopNavItem.Life -> {} + UgcTopNavItem.Car -> {} + UgcTopNavItem.Fashion -> {} + UgcTopNavItem.Sports -> {} + UgcTopNavItem.Animal -> {} + } + } + ) + } + ) { innerPadding -> + Box( + modifier = Modifier + .padding(innerPadding) + .onFocusChanged { focusOnContent = it.hasFocus } + ) { + AnimatedContent( + targetState = selectedTab, + label = "ugc animated content", + transitionSpec = { + val coefficient = 10 + if (targetState.ordinal < initialState.ordinal) { + fadeIn() + slideInHorizontally { -it / coefficient } togetherWith + fadeOut() + slideOutHorizontally { it / coefficient } + } else { + fadeIn() + slideInHorizontally { it / coefficient } togetherWith + fadeOut() + slideOutHorizontally { -it / coefficient } + } + } + ) { screen -> + when (screen) { + UgcTopNavItem.Douga -> DevelopingTipContent() + UgcTopNavItem.Game -> DevelopingTipContent() + UgcTopNavItem.Kichiku -> DevelopingTipContent() + UgcTopNavItem.Music -> DevelopingTipContent() + UgcTopNavItem.Dance -> DevelopingTipContent() + UgcTopNavItem.Cinephile -> DevelopingTipContent() + UgcTopNavItem.Ent -> DevelopingTipContent() + UgcTopNavItem.Knowledge -> DevelopingTipContent() + UgcTopNavItem.Tech -> DevelopingTipContent() + UgcTopNavItem.Information -> DevelopingTipContent() + UgcTopNavItem.Food -> DevelopingTipContent() + UgcTopNavItem.Life -> DevelopingTipContent() + UgcTopNavItem.Car -> DevelopingTipContent() + UgcTopNavItem.Fashion -> DevelopingTipContent() + UgcTopNavItem.Sports -> DevelopingTipContent() + UgcTopNavItem.Animal -> DevelopingTipContent() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/DynamicsScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/DynamicsScreen.kt similarity index 83% rename from app/src/main/kotlin/dev/aaa1115910/bv/screen/home/DynamicsScreen.kt rename to app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/DynamicsScreen.kt index a56e2649..71cc7457 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/DynamicsScreen.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/DynamicsScreen.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.bv.screen.home +package dev.aaa1115910.bv.screen.main.home import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -42,7 +42,6 @@ import org.koin.androidx.compose.koinViewModel fun DynamicsScreen( modifier: Modifier = Modifier, lazyGridState: LazyGridState, - onBackNav: () -> Unit, dynamicViewModel: DynamicViewModel = koinViewModel() ) { val context = LocalContext.current @@ -65,27 +64,7 @@ fun DynamicsScreen( if (dynamicViewModel.isLogin) { LazyVerticalGrid( - modifier = modifier - .onPreviewKeyEvent { - when (it.key) { - Key.Back -> { - if (it.type == KeyEventType.KeyUp) { - scope.launch(Dispatchers.Main) { - lazyGridState.animateScrollToItem(0) - } - onBackNav() - } - return@onPreviewKeyEvent true - } - - Key.DirectionRight -> { - if (currentFocusedIndex % 4 == 3) { - return@onPreviewKeyEvent true - } - } - } - return@onPreviewKeyEvent false - }, + modifier = modifier, state = lazyGridState, columns = GridCells.Fixed(4), contentPadding = PaddingValues(24.dp), diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/PopularScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/PopularScreen.kt similarity index 81% rename from app/src/main/kotlin/dev/aaa1115910/bv/screen/home/PopularScreen.kt rename to app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/PopularScreen.kt index d85fea44..15d294fd 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/PopularScreen.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/PopularScreen.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.bv.screen.home +package dev.aaa1115910.bv.screen.main.home import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -39,7 +39,6 @@ import org.koin.androidx.compose.koinViewModel fun PopularScreen( modifier: Modifier = Modifier, lazyGridState: LazyGridState, - onBackNav: () -> Unit, popularViewModel: PopularViewModel = koinViewModel() ) { val context = LocalContext.current @@ -60,27 +59,7 @@ fun PopularScreen( } LazyVerticalGrid( - modifier = modifier - .onPreviewKeyEvent { - when (it.key) { - Key.Back -> { - if (it.type == KeyEventType.KeyUp) { - scope.launch(Dispatchers.Main) { - lazyGridState.animateScrollToItem(0) - } - onBackNav() - } - return@onPreviewKeyEvent true - } - - Key.DirectionRight -> { - if (currentFocusedIndex % 4 == 3) { - return@onPreviewKeyEvent true - } - } - } - return@onPreviewKeyEvent false - }, + modifier = modifier, state = lazyGridState, columns = GridCells.Fixed(4), contentPadding = PaddingValues(24.dp), diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/RecommendScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/RecommendScreen.kt similarity index 81% rename from app/src/main/kotlin/dev/aaa1115910/bv/screen/home/RecommendScreen.kt rename to app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/RecommendScreen.kt index eb90c3d2..a1b69be6 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/RecommendScreen.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/home/RecommendScreen.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.bv.screen.home +package dev.aaa1115910.bv.screen.main.home import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -39,7 +39,6 @@ import org.koin.androidx.compose.koinViewModel fun RecommendScreen( modifier: Modifier = Modifier, lazyGridState: LazyGridState, - onBackNav: () -> Unit, recommendViewModel: RecommendViewModel = koinViewModel() ) { val context = LocalContext.current @@ -61,27 +60,7 @@ fun RecommendScreen( } LazyVerticalGrid( - modifier = modifier - .onPreviewKeyEvent { - when (it.key) { - Key.Back -> { - if (it.type == KeyEventType.KeyUp) { - scope.launch(Dispatchers.Main) { - lazyGridState.animateScrollToItem(0) - } - onBackNav() - } - return@onPreviewKeyEvent true - } - - Key.DirectionRight -> { - if (currentFocusedIndex % 4 == 3) { - return@onPreviewKeyEvent true - } - } - } - return@onPreviewKeyEvent false - }, + modifier = modifier, state = lazyGridState, columns = GridCells.Fixed(4), contentPadding = PaddingValues(24.dp), diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/AnimeScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/AnimeContent.kt similarity index 92% rename from app/src/main/kotlin/dev/aaa1115910/bv/screen/home/AnimeScreen.kt rename to app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/AnimeContent.kt index 6cca1a01..0d7fa646 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/AnimeScreen.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/AnimeContent.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.bv.screen.home +package dev.aaa1115910.bv.screen.main.pgc import android.content.Intent import android.view.KeyEvent @@ -8,6 +8,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -25,17 +26,21 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.List import androidx.compose.material.icons.rounded.Alarm import androidx.compose.material.icons.rounded.Favorite import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush @@ -44,10 +49,8 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.key.type import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -79,50 +82,43 @@ import dev.aaa1115910.bv.util.focusedBorder import dev.aaa1115910.bv.util.resizedImageUrl import dev.aaa1115910.bv.util.toast import dev.aaa1115910.bv.viewmodel.home.AnimeViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel @Composable -fun AnimeScreen( +fun AnimeContent( modifier: Modifier = Modifier, lazyListState: LazyListState, - onBackNav: () -> Unit, animeViewModel: AnimeViewModel = koinViewModel() ) { val context = LocalContext.current - val scope = rememberCoroutineScope() + val carouselFocusRequester = remember { FocusRequester() } val carouselItems = animeViewModel.carouselItems val animeFeeds = animeViewModel.feedItems LazyColumn( - modifier = modifier - .onPreviewKeyEvent { - when (it.key) { - Key.Back -> { - if (it.type == KeyEventType.KeyUp) { - scope.launch(Dispatchers.Main) { - lazyListState.animateScrollToItem(0) - } - onBackNav() - } - return@onPreviewKeyEvent true - } - } - return@onPreviewKeyEvent false - }, + modifier = modifier, state = lazyListState ) { item { - AnimeCarousel( - modifier = Modifier.padding(32.dp, 0.dp), - data = carouselItems - ) + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.Center + ) { + AnimeCarousel( + modifier = Modifier + .width(880.dp) + .padding(32.dp, 0.dp) + .focusRequester(carouselFocusRequester), + data = carouselItems + ) + } } item { AnimeFeatureButtons( - modifier = Modifier.padding(32.dp, 24.dp), + modifier = Modifier.padding(vertical = 24.dp), onOpenTimeline = { context.startActivity(Intent(context, AnimeTimelineActivity::class.java)) }, @@ -182,7 +178,7 @@ fun AnimeCarousel( Carousel( itemCount = data.size, modifier = modifier - .fillMaxWidth() + //.fillMaxWidth() .height(240.dp) .clip(MaterialTheme.shapes.large) .focusedBorder(), @@ -231,6 +227,7 @@ private fun AnimeFeatureButtons( onOpenIndex: () -> Unit, onOpenGamerAni: () -> Unit = {} ) { + val buttonWidth = 185.dp val buttons = listOf( Triple( stringResource(R.string.anime_home_button_timeline), @@ -254,21 +251,24 @@ private fun AnimeFeatureButtons( ) ) - Row( - modifier = modifier.height(80.dp), - horizontalArrangement = Arrangement.spacedBy(24.dp) + LazyRow( + modifier = modifier + .fillMaxWidth() + .height(80.dp), + horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally), + contentPadding = PaddingValues(horizontal = 32.dp) ) { - buttons.forEach { (title, icon, onClick) -> + items(items = buttons) { (title, icon, onClick) -> when (icon) { is ImageVector -> AnimeFeatureButton( - modifier = Modifier.weight(1f), + modifier = Modifier.width(buttonWidth), title = title, icon = icon, onClick = onClick ) is Painter -> AnimeFeatureButton( - modifier = Modifier.weight(1f), + modifier = Modifier.width(buttonWidth), title = title, icon = icon, onClick = onClick @@ -363,7 +363,7 @@ fun AnimeFeedVideoRow( LazyRow( modifier = modifier, contentPadding = PaddingValues(horizontal = 24.dp), - horizontalArrangement = Arrangement.spacedBy(18.dp) + horizontalArrangement = Arrangement.spacedBy(24.dp) ) { data.forEachIndexed { index, feedItem -> val cardModifier = if (index == data.lastIndex) { diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/anime/AnimeIndexScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/anime/AnimeIndexScreen.kt similarity index 99% rename from app/src/main/kotlin/dev/aaa1115910/bv/screen/home/anime/AnimeIndexScreen.kt rename to app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/anime/AnimeIndexScreen.kt index 89a3847c..a6ee441f 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/anime/AnimeIndexScreen.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/anime/AnimeIndexScreen.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.bv.screen.home.anime +package dev.aaa1115910.bv.screen.main.pgc.anime import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/anime/AnimeTimelineScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/anime/AnimeTimelineScreen.kt similarity index 99% rename from app/src/main/kotlin/dev/aaa1115910/bv/screen/home/anime/AnimeTimelineScreen.kt rename to app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/anime/AnimeTimelineScreen.kt index c595ed14..cf1de93d 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/home/anime/AnimeTimelineScreen.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/main/pgc/anime/AnimeTimelineScreen.kt @@ -1,4 +1,4 @@ -package dev.aaa1115910.bv.screen.home.anime +package dev.aaa1115910.bv.screen.main.pgc.anime import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/screen/search/SearchInputScreen.kt b/app/src/main/kotlin/dev/aaa1115910/bv/screen/search/SearchInputScreen.kt index f64a401e..33b94233 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/screen/search/SearchInputScreen.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/screen/search/SearchInputScreen.kt @@ -1,15 +1,17 @@ package dev.aaa1115910.bv.screen.search +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.OutlinedTextField @@ -44,10 +46,10 @@ import org.koin.androidx.compose.koinViewModel @Composable fun SearchInputScreen( modifier: Modifier = Modifier, + defaultFocusRequester: FocusRequester, searchInputViewModel: SearchInputViewModel = koinViewModel() ) { val context = LocalContext.current - val softKeyboardFirstButtonFocusRequester = remember { FocusRequester() } val hotsFocusRestorerModifiers = createCustomInitialFocusRestorerModifiers() val historyFocusRestorerModifiers = createCustomInitialFocusRestorerModifiers() val suggestFocusRestorerModifiers = createCustomInitialFocusRestorerModifiers() @@ -69,12 +71,6 @@ fun SearchInputScreen( searchInputViewModel.updateSuggests() } - LaunchedEffect(Unit) { - runCatching { - softKeyboardFirstButtonFocusRequester.requestFocus() - } - } - Scaffold( modifier = modifier, topBar = { @@ -97,13 +93,15 @@ fun SearchInputScreen( Row( modifier = Modifier .padding(innerPadding) - .padding(horizontal = 24.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) + .padding(vertical = 8.dp) + .padding(start = 24.dp) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(20.dp) ) { Box( modifier = Modifier - .weight(1f) - .fillMaxSize(), + .width(280.dp) + .fillMaxHeight(), contentAlignment = Alignment.TopCenter ) { Column( @@ -124,7 +122,7 @@ fun SearchInputScreen( ) ) SoftKeyboard( - firstButtonFocusRequester = softKeyboardFirstButtonFocusRequester, + firstButtonFocusRequester = defaultFocusRequester, showSearchWithProxy = Prefs.enableProxy, enableSearchWithProxy = enableProxy, onClick = { @@ -151,8 +149,8 @@ fun SearchInputScreen( if (searchKeyword.isEmpty()) { Column( modifier = Modifier - .weight(1f) - .fillMaxSize() + .width(250.dp) + .fillMaxHeight(), ) { Text( modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), @@ -177,8 +175,8 @@ fun SearchInputScreen( } else { Column( modifier = Modifier - .weight(1f) - .fillMaxSize() + .width(250.dp) + .fillMaxHeight(), ) { Text( modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), @@ -207,8 +205,9 @@ fun SearchInputScreen( Column( modifier = Modifier - .weight(1f) - .fillMaxSize() + .width(250.dp) + .fillMaxHeight() + .padding(end = 10.dp), ) { Text( modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/util/Extends.kt b/app/src/main/kotlin/dev/aaa1115910/bv/util/Extends.kt index f41228ed..c1c920b4 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/util/Extends.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/util/Extends.kt @@ -5,6 +5,11 @@ import android.widget.Toast import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type import androidx.core.text.HtmlCompat import dev.aaa1115910.bv.BVApp import dev.aaa1115910.bv.R @@ -99,4 +104,11 @@ fun FocusRequester.requestFocus(scope: CoroutineScope) { fun String.removeHtmlTags(): String = HtmlCompat.fromHtml( this, HtmlCompat.FROM_HTML_MODE_LEGACY -).toString() \ No newline at end of file +).toString() + +fun KeyEvent.isKeyDown(): Boolean = type == KeyEventType.KeyDown +fun KeyEvent.isKeyUp(): Boolean = type == KeyEventType.KeyUp +fun KeyEvent.isDpadUp(): Boolean = key == Key.DirectionUp +fun KeyEvent.isDpadDown(): Boolean = key == Key.DirectionDown +fun KeyEvent.isDpadLeft(): Boolean = key == Key.DirectionLeft +fun KeyEvent.isDpadRight(): Boolean = key == Key.DirectionRight \ No newline at end of file diff --git a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/AnimeViewModel.kt b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/AnimeViewModel.kt index 051b3d89..e347a791 100644 --- a/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/AnimeViewModel.kt +++ b/app/src/main/kotlin/dev/aaa1115910/bv/viewmodel/home/AnimeViewModel.kt @@ -55,7 +55,7 @@ class AnimeViewModel : ViewModel() { fun reloadAll() { logger.fInfo { "Reload all" } clearAll() - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(Dispatchers.IO) { updateCarousel() updateFeed() } @@ -103,8 +103,8 @@ class AnimeViewModel : ViewModel() { } } - vCardList.chunked(6).forEach { chunkedVCardList -> - if (chunkedVCardList.size == 6) { + vCardList.chunked(5).forEach { chunkedVCardList -> + if (chunkedVCardList.size == 5) { feedItems.add(chunkedVCardList) } else { restSubItems.clear()