diff --git a/app/src/main/kotlin/com/muedsa/agetv/model/dandanplay/DanEpisode.kt b/app/src/main/kotlin/com/muedsa/agetv/model/dandanplay/DanEpisode.kt index 2b8175a..dbf0a97 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/model/dandanplay/DanEpisode.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/model/dandanplay/DanEpisode.kt @@ -9,5 +9,5 @@ data class DanEpisode( @SerialName("episodeTitle") val episodeTitle: String, @SerialName("episodeNumber") val episodeNumber: String, @SerialName("lastWatched") val lastWatched: String? = null, - @SerialName("airDate") val airDate: String + @SerialName("airDate") val airDate: String? = null, ) \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/HomeNavTab.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/HomeNavTab.kt index e38ce8a..8194815 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/HomeNavTab.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/HomeNavTab.kt @@ -24,6 +24,7 @@ import androidx.tv.material3.TabRow import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.Text import com.muedsa.agetv.ui.features.home.catalog.CatalogScreen +import com.muedsa.agetv.ui.features.home.latest.LatestUpdateScreen import com.muedsa.agetv.ui.features.home.main.MainScreen import com.muedsa.agetv.ui.features.home.rank.RankScreen import com.muedsa.agetv.ui.features.home.search.SearchScreen @@ -39,6 +40,7 @@ import kotlin.time.Duration.Companion.milliseconds val tabs = listOf( HomeNavTabs.Main, HomeNavTabs.Rank, + HomeNavTabs.Latest, HomeNavTabs.Search, HomeNavTabs.Catalog ) @@ -140,13 +142,19 @@ fun HomeContent( onNavigate = onNavigate ) - 2 -> SearchScreen( + 2 -> LatestUpdateScreen( backgroundState = backgroundState, errorMsgBoxState = errorMsgBoxState, onNavigate = onNavigate ) - 3 -> CatalogScreen( + 3 -> SearchScreen( + backgroundState = backgroundState, + errorMsgBoxState = errorMsgBoxState, + onNavigate = onNavigate + ) + + 4 -> CatalogScreen( backgroundState = backgroundState, errorMsgBoxState = errorMsgBoxState, onNavigate = onNavigate diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/HomeNavTabs.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/HomeNavTabs.kt index 58d7719..dbb5b37 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/HomeNavTabs.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/HomeNavTabs.kt @@ -2,9 +2,8 @@ package com.muedsa.agetv.ui.features.home sealed class HomeNavTabs(val title: String) { data object Main : HomeNavTabs("首页") - data object Rank : HomeNavTabs("排行") - + data object Latest : HomeNavTabs("更新") data object Search : HomeNavTabs("搜索") data object Catalog : HomeNavTabs("目录") } \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/latest/LatestUpdateScreen.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/latest/LatestUpdateScreen.kt new file mode 100644 index 0000000..d7ead16 --- /dev/null +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/latest/LatestUpdateScreen.kt @@ -0,0 +1,171 @@ +package com.muedsa.agetv.ui.features.home.latest + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.tv.foundation.lazy.grid.TvGridCells +import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid +import androidx.tv.foundation.lazy.grid.itemsIndexed +import androidx.tv.material3.Card +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import com.muedsa.agetv.model.LazyType +import com.muedsa.agetv.ui.AgePosterSize +import com.muedsa.agetv.ui.navigation.NavigationItems +import com.muedsa.agetv.viewmodel.LatestUpdateViewModel +import com.muedsa.compose.tv.model.ContentModel +import com.muedsa.compose.tv.theme.CardContentPadding +import com.muedsa.compose.tv.theme.ImageCardRowCardPadding +import com.muedsa.compose.tv.theme.ScreenPaddingLeft +import com.muedsa.compose.tv.widget.CardType +import com.muedsa.compose.tv.widget.ErrorMessageBoxState +import com.muedsa.compose.tv.widget.ImageContentCard +import com.muedsa.compose.tv.widget.ScreenBackgroundState +import com.muedsa.compose.tv.widget.ScreenBackgroundType +import com.muedsa.uitl.LogUtil + +@OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class) +@Composable +fun LatestUpdateScreen( + viewModel: LatestUpdateViewModel = hiltViewModel(), + backgroundState: ScreenBackgroundState, + errorMsgBoxState: ErrorMessageBoxState, + onNavigate: (NavigationItems, List?) -> Unit = { _, _ -> } +) { + val fontScale = LocalConfiguration.current.fontScale + val titleMediumFontSize = MaterialTheme.typography.titleMedium.fontSize.value + val bodyMediumFontSize = MaterialTheme.typography.bodyMedium.fontSize.value + val contentHeight = remember { + (titleMediumFontSize * fontScale + 0.5f).dp + + (bodyMediumFontSize * fontScale + 0.5f).dp + + CardContentPadding * 2 + } + + val latestUpdateLP by remember { viewModel.latestUpdateLPState } + + LaunchedEffect(key1 = latestUpdateLP) { + if (latestUpdateLP.type == LazyType.FAILURE) { + errorMsgBoxState.error(latestUpdateLP.error) + } + } + + Column(modifier = Modifier.padding(start = ScreenPaddingLeft)) { + Text( + text = "最近更新", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.titleLarge + ) + + if (latestUpdateLP.list.isNotEmpty()) { + val gridFocusRequester = remember { FocusRequester() } + + TvLazyVerticalGrid( + modifier = Modifier + .padding(start = 0.dp, top = 20.dp, end = 20.dp, bottom = 20.dp) + .focusRequester(gridFocusRequester) + .focusProperties { + exit = { gridFocusRequester.saveFocusedChild(); FocusRequester.Default } + enter = { + if (gridFocusRequester.restoreFocusedChild()) { + LogUtil.d("grid restoreFocusedChild") + FocusRequester.Cancel + } else { + LogUtil.d("grid focused default child") + FocusRequester.Default + } + } + }, + columns = TvGridCells.Adaptive(AgePosterSize.width + ImageCardRowCardPadding), + contentPadding = PaddingValues( + top = ImageCardRowCardPadding, + bottom = ImageCardRowCardPadding + ) + ) { + itemsIndexed( + items = latestUpdateLP.list, + key = { _, item -> item.aid } + ) { index, item -> + val itemFocusRequester = remember { + FocusRequester() + } + ImageContentCard( + modifier = Modifier + .padding(end = ImageCardRowCardPadding) + .focusRequester(itemFocusRequester), + url = item.picSmall, + imageSize = AgePosterSize, + type = CardType.STANDARD, + model = ContentModel( + item.title, + subtitle = item.newTitle + ), + onItemFocus = { + backgroundState.url = item.picSmall + backgroundState.type = ScreenBackgroundType.BLUR + }, + onItemClick = { + LogUtil.d("Click $item") + onNavigate(NavigationItems.Detail, listOf(item.aid.toString())) + } + ) + + LaunchedEffect(key1 = Unit) { + if (latestUpdateLP.offset == index) { + itemFocusRequester.requestFocus() + } + } + } + + if (latestUpdateLP.type != LazyType.LOADING && latestUpdateLP.hasNext) { + item { + Column { + Card( + modifier = Modifier + .size(AgePosterSize) + .padding(end = ImageCardRowCardPadding), + onClick = { + viewModel.fetchLatestUpdate() + } + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "继续加载") + } + } + Spacer(modifier = Modifier.height(contentHeight)) + } + } + } + } + } + } + + + + LaunchedEffect(key1 = Unit) { + viewModel.fetchLatestUpdate() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/LatestUpdateViewModel.kt b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/LatestUpdateViewModel.kt new file mode 100644 index 0000000..783979a --- /dev/null +++ b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/LatestUpdateViewModel.kt @@ -0,0 +1,47 @@ +package com.muedsa.agetv.viewmodel + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.muedsa.agetv.model.LazyPagedList +import com.muedsa.agetv.model.age.PosterAnimeModel +import com.muedsa.agetv.repository.AppRepository +import com.muedsa.uitl.LogUtil +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject +import kotlin.math.ceil + +@HiltViewModel +class LatestUpdateViewModel @Inject constructor( + private val repo: AppRepository +) : ViewModel() { + + val latestUpdateLPState = mutableStateOf>(LazyPagedList.new()) + + fun fetchLatestUpdate() { + val nextPage = latestUpdateLPState.value.nextPage + latestUpdateLPState.value = latestUpdateLPState.value.loadingNext() + viewModelScope.launch(context = Dispatchers.IO) { + try { + repo.update(nextPage, PAGE_SIZE).let { + latestUpdateLPState.value = latestUpdateLPState.value.successNext( + it.videos, + ceil(it.total.toDouble() / CatalogViewModel.PAGE_SIZE).toInt() + ) + } + } catch (t: Throwable) { + withContext(Dispatchers.Main) { + latestUpdateLPState.value = latestUpdateLPState.value.failNext(t) + } + LogUtil.fb(t) + } + } + } + + companion object { + const val PAGE_SIZE = 30 + } +} \ No newline at end of file