diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e53f059..8d9bd56 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -101,6 +101,7 @@ dependencies { implementation(libs.core.ktx) implementation(libs.core.splashscreen) implementation(libs.lifecycle.runtime.ktx) + implementation(libs.lifecycle.runtime.compose) implementation(libs.lifecycle.viewmodel.compose) implementation(libs.hilt.android) kapt(libs.hilt.compiler) diff --git a/app/src/main/kotlin/com/muedsa/agetv/MainActivity.kt b/app/src/main/kotlin/com/muedsa/agetv/MainActivity.kt index 85dc7fe..ac015c8 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/MainActivity.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/MainActivity.kt @@ -31,11 +31,9 @@ class MainActivity : ComponentActivity() { val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) splashScreen.setKeepOnScreenCondition { - homePageViewModel.homeDataState.value.type == LazyType.LOADING + homePageViewModel.homeDataSF.value.type == LazyType.LOADING } - homePageViewModel.fetchHome() setContent { - TvTheme { // A surface container using the 'background' color from the theme Surface( @@ -52,7 +50,6 @@ class MainActivity : ComponentActivity() { errorMsgBoxState = errorMsgBoxState ) } - } } } diff --git a/app/src/main/kotlin/com/muedsa/agetv/model/CatalogOptionsUIModel.kt b/app/src/main/kotlin/com/muedsa/agetv/model/CatalogOptionsUIModel.kt new file mode 100644 index 0000000..0ace8f7 --- /dev/null +++ b/app/src/main/kotlin/com/muedsa/agetv/model/CatalogOptionsUIModel.kt @@ -0,0 +1,27 @@ +package com.muedsa.agetv.model + +import com.muedsa.agetv.model.age.AgeCatalogOption + +data class CatalogOptionsUIModel( + val order: AgeCatalogOption = AgeCatalogOption.Order[0], + val region: AgeCatalogOption = AgeCatalogOption.Regions[0], + val genre: AgeCatalogOption = AgeCatalogOption.Genres[0], + val year: AgeCatalogOption = AgeCatalogOption.Years[0], + val season: AgeCatalogOption = AgeCatalogOption.Seasons[0], + val status: AgeCatalogOption = AgeCatalogOption.Status[0], + val label: AgeCatalogOption = AgeCatalogOption.Labels[0], + val resource: AgeCatalogOption = AgeCatalogOption.Resources[0], +) { + + fun default(): CatalogOptionsUIModel { + return if (this == Default) { + this + } else { + Default + } + } + + companion object { + val Default = CatalogOptionsUIModel() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/model/LazyPagedList.kt b/app/src/main/kotlin/com/muedsa/agetv/model/LazyPagedList.kt index c6d45ca..b23b1cf 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/model/LazyPagedList.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/model/LazyPagedList.kt @@ -1,7 +1,8 @@ package com.muedsa.agetv.model -data class LazyPagedList( - val list: MutableList = mutableListOf(), +data class LazyPagedList( + val query: Q, + val list: MutableList = mutableListOf(), val page: Int = 0, val totalPage: Int = 0, val offset: Int = 0, @@ -12,13 +13,15 @@ data class LazyPagedList( val hasNext get() = page == 0 || page < totalPage fun loadingNext() = LazyPagedList( + query = query, list = list, page = page, totalPage = totalPage, type = LazyType.LOADING ) - fun successNext(appendList: List, totalPage: Int) = LazyPagedList( + fun successNext(appendList: List, totalPage: Int) = LazyPagedList( + query = query, offset = list.size, list = list.also { it.addAll(appendList) }, page = nextPage, @@ -27,6 +30,7 @@ data class LazyPagedList( ) fun failNext(error: Throwable?) = LazyPagedList( + query = query, list = list, page = page, totalPage = totalPage, @@ -36,6 +40,7 @@ data class LazyPagedList( companion object { @JvmStatic - fun new(): LazyPagedList = LazyPagedList(type = LazyType.SUCCESS) + fun new(query: Q): LazyPagedList = + LazyPagedList(query = query, type = LazyType.SUCCESS) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/model/age/AgeCatalogOption.kt b/app/src/main/kotlin/com/muedsa/agetv/model/age/AgeCatalogOption.kt index fe035b8..a500567 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/model/age/AgeCatalogOption.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/model/age/AgeCatalogOption.kt @@ -1,7 +1,5 @@ package com.muedsa.agetv.model.age -import java.util.Collections - sealed class AgeCatalogOption(val text: String, val value: String) { data object ALL : AgeCatalogOption("全部", "all") @@ -100,22 +98,18 @@ sealed class AgeCatalogOption(val text: String, val value: String) { data object OrderByHits : AgeCatalogOption("点击量", "hits") companion object { - val Regions = - listOf(ALL, RegionJP, RegionCN, RegionWW).let { Collections.unmodifiableList(it) } + val Regions = listOf(ALL, RegionJP, RegionCN, RegionWW) - val Genres = - listOf(ALL, GenreTV, GenreMovie, GenreOVA).let { Collections.unmodifiableList(it) } + val Genres = listOf(ALL, GenreTV, GenreMovie, GenreOVA) - val Seasons = listOf(ALL, SeasonQ1, SeasonQ2, SeasonQ3, SeasonQ4).let { - Collections.unmodifiableList(it) - } + val Seasons = listOf(ALL, SeasonQ1, SeasonQ2, SeasonQ3, SeasonQ4) val Status = listOf( ALL, StatueSerializing, StatueCompleteD, StatueNotPlay - ).let { Collections.unmodifiableList(it) } + ) val Years = listOf( ALL, @@ -143,7 +137,7 @@ sealed class AgeCatalogOption(val text: String, val value: String) { Year2002, Year2001, Year2000AndBefore, - ).let { Collections.unmodifiableList(it) } + ) val Labels = listOf( ALL, @@ -191,12 +185,10 @@ sealed class AgeCatalogOption(val text: String, val value: String) { LabelFemaleOriented, LabelShort, LabelHappy - ).let { Collections.unmodifiableList(it) } + ) - val Resources = - listOf(ALL, ResourceBDRIP, ResourceGERIP).let { Collections.unmodifiableList(it) } + val Resources = listOf(ALL, ResourceBDRIP, ResourceGERIP) - val Order = - listOf(OrderByTime, OrderByName, OrderByHits).let { Collections.unmodifiableList(it) } + val Order = listOf(OrderByTime, OrderByName, OrderByHits) } } diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/detail/AnimeDetailScreen.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/detail/AnimeDetailScreen.kt index de87cc8..4402d78 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/detail/AnimeDetailScreen.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/detail/AnimeDetailScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Whatshot import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -58,6 +59,7 @@ import com.muedsa.compose.tv.widget.ScreenBackgroundType import com.muedsa.compose.tv.widget.StandardImageCardsRow import com.muedsa.compose.tv.widget.rememberScreenBackgroundState import com.muedsa.uitl.LogUtil +import kotlinx.coroutines.flow.update @OptIn( ExperimentalTvMaterial3Api::class, @@ -73,11 +75,9 @@ fun AnimeDetailScreen( val screenHeight = configuration.screenHeightDp.dp val screenWidth = configuration.screenWidthDp.dp - val animeDetailLD by remember { viewModel.animeDetailLDState } - val commentsLP by remember { viewModel.commentsLPState } - - val danSearchAnimeListLD by remember { viewModel.danSearchAnimeListLDState } - val danAnimeInfoLD by remember { viewModel.danAnimeInfoLDState } + val animeDetailLD by viewModel.animeDetailLDSF.collectAsState() + val danSearchAnimeListLD by viewModel.danSearchAnimeListLDSF.collectAsState() + val danAnimeInfoLD by viewModel.danAnimeInfoLDSF.collectAsState() val backgroundState = rememberScreenBackgroundState( initType = ScreenBackgroundType.SCRIM @@ -89,17 +89,10 @@ fun AnimeDetailScreen( } else if (animeDetailLD.type == LazyType.SUCCESS) { if (animeDetailLD.data != null) { backgroundState.url = animeDetailLD.data!!.video.cover - viewModel.searchDanAnime() } } } - LaunchedEffect(key1 = commentsLP) { - if (commentsLP.type == LazyType.FAILURE) { - errorMsgBoxState.error(commentsLP.error) - } - } - LaunchedEffect(key1 = danSearchAnimeListLD) { if (danSearchAnimeListLD.type == LazyType.FAILURE) { errorMsgBoxState.error(danSearchAnimeListLD.error) @@ -281,7 +274,7 @@ fun AnimeDetailScreen( item.animeTitle }, onSelected = { _, item -> - viewModel.fetchDanBangumi(item.animeId) + viewModel.danBangumi(item.animeId) } ) } @@ -377,7 +370,9 @@ fun AnimeDetailScreen( }, onItemClick = { _, anime -> LogUtil.fb("Click $anime") - viewModel.animeIdLD.value = anime.aid.toString() + viewModel.animeIdSF.update { + anime.aid.toString() + } } ) Spacer(modifier = Modifier.height(25.dp)) @@ -399,7 +394,9 @@ fun AnimeDetailScreen( }, onItemClick = { _, anime -> LogUtil.fb("Click $anime") - viewModel.animeIdLD.value = anime.aid.toString() + viewModel.animeIdSF.update { + anime.aid.toString() + } } ) Spacer(modifier = Modifier.height(25.dp)) @@ -418,11 +415,9 @@ fun AnimeDetailScreen( } } else if (animeDetailLD.type == LazyType.FAILURE) { ErrorScreen { - viewModel.animeIdLD.value = viewModel.animeIdLD.value + viewModel.animeIdSF.value = viewModel.animeIdSF.value } } else { LoadingScreen() } - - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/catalog/CatalogOptionsWidget.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/catalog/CatalogOptionsWidget.kt index e8786bb..632138a 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/catalog/CatalogOptionsWidget.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/catalog/CatalogOptionsWidget.kt @@ -10,7 +10,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check import androidx.compose.material3.Divider import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.tv.material3.ExperimentalTvMaterial3Api @@ -25,9 +24,9 @@ import com.muedsa.agetv.model.age.AgeCatalogOption @Composable fun CatalogOptionsWidget( title: String, - state: MutableState, + selectedIndex: Int, options: List, - onChange: () -> Unit = {} + onClick: (Int, AgeCatalogOption) -> Unit = { _, _ -> } ) { Text( text = title, @@ -36,11 +35,11 @@ fun CatalogOptionsWidget( ) Spacer(modifier = Modifier.height(4.dp)) FlowRow { - for (option in options) { + options.forEachIndexed { index, option -> FilterChip( modifier = Modifier.padding(8.dp), - selected = option == state.value, - leadingIcon = if (option == state.value) { + selected = index == selectedIndex, + leadingIcon = if (index == selectedIndex) { { Icon( modifier = Modifier.size(FilterChipDefaults.IconSize), @@ -50,12 +49,7 @@ fun CatalogOptionsWidget( } } else null, onClick = { - state.value = if (option == state.value) { - AgeCatalogOption.Order[0] - } else { - option - } - onChange() + onClick(index, option) } ) { Text(text = option.text) diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/catalog/CatalogScreen.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/catalog/CatalogScreen.kt index 77f59e8..4929519 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/catalog/CatalogScreen.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/catalog/CatalogScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material.icons.outlined.KeyboardArrowUp import androidx.compose.material.icons.outlined.Refresh import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,7 +45,6 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.OutlinedButton import androidx.tv.material3.OutlinedIconButton import androidx.tv.material3.Text -import com.muedsa.agetv.model.LazyPagedList import com.muedsa.agetv.model.LazyType import com.muedsa.agetv.model.age.AgeCatalogOption import com.muedsa.agetv.ui.AgePosterSize @@ -78,32 +78,16 @@ fun CatalogScreen( CardContentPadding * 2 } - - val orderState = remember { viewModel.orderState } - val regionState = remember { viewModel.regionState } - val genreState = remember { viewModel.genreState } - val yearState = remember { viewModel.yearState } - val seasonState = remember { viewModel.seasonState } - val statusState = remember { viewModel.statusState } - val labelState = remember { viewModel.labelState } - val resourceState = remember { viewModel.resourceState } - - val searchAnimeLPState by remember { viewModel.animeLPState } - - val handleFetchCatalog = remember { - { - viewModel.animeLPState.value = LazyPagedList.new() - viewModel.fetchAnimeCatalog() - } - } + val query by viewModel.querySF.collectAsState() + val searchAnimeLP by viewModel.animeLPSF.collectAsState() var optionsExpand by remember { mutableStateOf(false) } - LaunchedEffect(key1 = searchAnimeLPState) { - if (searchAnimeLPState.type == LazyType.FAILURE) { - errorMsgBoxState.error(searchAnimeLPState.error) + LaunchedEffect(key1 = searchAnimeLP) { + if (searchAnimeLP.type == LazyType.FAILURE) { + errorMsgBoxState.error(searchAnimeLP.error) } } @@ -134,7 +118,6 @@ fun CatalogScreen( Spacer(modifier = Modifier.width(16.dp)) OutlinedIconButton(onClick = { viewModel.resetCatalogOptions() - viewModel.fetchAnimeCatalog() }) { Icon( modifier = Modifier.size(ButtonDefaults.IconSize), @@ -149,146 +132,210 @@ fun CatalogScreen( TvLazyColumn(contentPadding = PaddingValues(top = ImageCardRowCardPadding)) { item { CatalogOptionsWidget( - "排序", - orderState, - AgeCatalogOption.Order, - handleFetchCatalog + title = "排序", + selectedIndex = AgeCatalogOption.Order.indexOf(query.order), + options = AgeCatalogOption.Order, + onClick = { _, option -> + if (query.order == option) { + viewModel.querySF.value = + query.copy(order = AgeCatalogOption.Order[0]) + } else { + viewModel.querySF.value = query.copy(order = option) + } + } ) } item { CatalogOptionsWidget( - "区域", regionState, AgeCatalogOption.Regions, - handleFetchCatalog + title = "区域", + selectedIndex = AgeCatalogOption.Regions.indexOf(query.region), + options = AgeCatalogOption.Regions, + onClick = { _, option -> + if (query.region == option) { + viewModel.querySF.value = + query.copy(region = AgeCatalogOption.Regions[0]) + } else { + viewModel.querySF.value = query.copy(region = option) + } + } ) } item { CatalogOptionsWidget( - "类型", genreState, AgeCatalogOption.Genres, - handleFetchCatalog + title = "类型", + selectedIndex = AgeCatalogOption.Genres.indexOf(query.genre), + options = AgeCatalogOption.Genres, + onClick = { _, option -> + if (query.genre == option) { + viewModel.querySF.value = + query.copy(genre = AgeCatalogOption.Genres[0]) + } else { + viewModel.querySF.value = query.copy(genre = option) + } + } ) } item { CatalogOptionsWidget( - "年份", yearState, AgeCatalogOption.Years, - handleFetchCatalog + title = "年份", + selectedIndex = AgeCatalogOption.Years.indexOf(query.year), + options = AgeCatalogOption.Years, + onClick = { _, option -> + if (query.year == option) { + viewModel.querySF.value = + query.copy(year = AgeCatalogOption.Years[0]) + } else { + viewModel.querySF.value = query.copy(year = option) + } + } ) } item { CatalogOptionsWidget( - "季度", seasonState, AgeCatalogOption.Seasons, - handleFetchCatalog + title = "季度", + selectedIndex = AgeCatalogOption.Seasons.indexOf(query.season), + options = AgeCatalogOption.Seasons, + onClick = { _, option -> + if (query.season == option) { + viewModel.querySF.value = + query.copy(season = AgeCatalogOption.Seasons[0]) + } else { + viewModel.querySF.value = query.copy(season = option) + } + } ) } item { CatalogOptionsWidget( - "状态", statusState, AgeCatalogOption.Status, - handleFetchCatalog + title = "状态", + selectedIndex = AgeCatalogOption.Status.indexOf(query.status), + options = AgeCatalogOption.Status, + onClick = { _, option -> + if (query.status == option) { + viewModel.querySF.value = + query.copy(status = AgeCatalogOption.Status[0]) + } else { + viewModel.querySF.value = query.copy(status = option) + } + } ) } item { CatalogOptionsWidget( - "标签", labelState, AgeCatalogOption.Labels, - handleFetchCatalog + title = "标签", + selectedIndex = AgeCatalogOption.Labels.indexOf(query.label), + options = AgeCatalogOption.Labels, + onClick = { _, option -> + if (query.label == option) { + viewModel.querySF.value = + query.copy(label = AgeCatalogOption.Labels[0]) + } else { + viewModel.querySF.value = query.copy(label = option) + } + } ) } item { CatalogOptionsWidget( - "资源", resourceState, AgeCatalogOption.Resources, - handleFetchCatalog + title = "资源", + selectedIndex = AgeCatalogOption.Resources.indexOf(query.resource), + options = AgeCatalogOption.Resources, + onClick = { _, option -> + if (query.resource == option) { + viewModel.querySF.value = + query.copy(resource = AgeCatalogOption.Resources[0]) + } else { + viewModel.querySF.value = query.copy(resource = option) + } + } ) } } } else { - if (searchAnimeLPState.list.isNotEmpty()) { - val gridFocusRequester = remember { FocusRequester() } + val gridFocusRequester = remember { FocusRequester() } - TvLazyVerticalGrid( - columns = TvGridCells.Adaptive(AgePosterSize.width + ImageCardRowCardPadding), - contentPadding = PaddingValues( - top = ImageCardRowCardPadding, - bottom = ImageCardRowCardPadding - ), - modifier = Modifier - .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 - } + TvLazyVerticalGrid( + columns = TvGridCells.Adaptive(AgePosterSize.width + ImageCardRowCardPadding), + contentPadding = PaddingValues( + top = ImageCardRowCardPadding, + bottom = ImageCardRowCardPadding + ), + modifier = Modifier + .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 } } - ) { - itemsIndexed( - items = searchAnimeLPState.list, - key = { _, item -> item.id } - ) { index, item -> - val itemFocusRequester = remember { - FocusRequester() + } + ) { + itemsIndexed( + items = searchAnimeLP.list, + key = { _, item -> item.id } + ) { index, item -> + val itemFocusRequester = remember { + FocusRequester() + } + ImageContentCard( + modifier = Modifier + .padding(end = ImageCardRowCardPadding) + .focusRequester(itemFocusRequester), + url = item.cover, + imageSize = AgePosterSize, + type = CardType.STANDARD, + model = ContentModel( + item.name, + subtitle = item.tags, + description = item.status + ), + onItemFocus = { + backgroundState.url = item.cover + backgroundState.type = ScreenBackgroundType.BLUR + }, + onItemClick = { + LogUtil.d("Click $item") + onNavigate(NavigationItems.Detail, listOf(item.id.toString())) } - ImageContentCard( - modifier = Modifier - .padding(end = ImageCardRowCardPadding) - .focusRequester(itemFocusRequester), - url = item.cover, - imageSize = AgePosterSize, - type = CardType.STANDARD, - model = ContentModel( - item.name, - subtitle = item.tags, - description = item.status - ), - onItemFocus = { - backgroundState.url = item.cover - backgroundState.type = ScreenBackgroundType.BLUR - }, - onItemClick = { - LogUtil.d("Click $item") - onNavigate(NavigationItems.Detail, listOf(item.id.toString())) - } - ) + ) - LaunchedEffect(key1 = Unit) { - if (searchAnimeLPState.offset == index) { - itemFocusRequester.requestFocus() - } + LaunchedEffect(key1 = Unit) { + if (searchAnimeLP.offset == index) { + itemFocusRequester.requestFocus() } } + } - if (searchAnimeLPState.type != LazyType.LOADING && searchAnimeLPState.hasNext) { - item { - Column { - Card( - modifier = Modifier - .size(AgePosterSize) - .padding(end = ImageCardRowCardPadding), - onClick = { - viewModel.fetchAnimeCatalog() - } + if (searchAnimeLP.type != LazyType.LOADING && searchAnimeLP.hasNext) { + item { + Column { + Card( + modifier = Modifier + .size(AgePosterSize) + .padding(end = ImageCardRowCardPadding), + onClick = { + viewModel.catalog(searchAnimeLP) + } + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = "继续加载") - } + Text(text = "继续加载") } - Spacer(modifier = Modifier.height(contentHeight)) } + Spacer(modifier = Modifier.height(contentHeight)) } } } } } } - - LaunchedEffect(key1 = Unit) { - viewModel.fetchAnimeCatalog() - } } 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 index d7ead16..690aa25 100644 --- 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 @@ -10,6 +10,7 @@ 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.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -60,7 +61,9 @@ fun LatestUpdateScreen( CardContentPadding * 2 } - val latestUpdateLP by remember { viewModel.latestUpdateLPState } + val latestUpdateLP by viewModel.latestUpdateLPSF.collectAsState() + + val gridFocusRequester = remember { FocusRequester() } LaunchedEffect(key1 = latestUpdateLP) { if (latestUpdateLP.type == LazyType.FAILURE) { @@ -75,97 +78,86 @@ fun LatestUpdateScreen( 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 - } + 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 }, - 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() + onItemClick = { + LogUtil.d("Click $item") + onNavigate(NavigationItems.Detail, listOf(item.aid.toString())) } - 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() - } + 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() - } + if (latestUpdateLP.type != LazyType.LOADING && latestUpdateLP.hasNext) { + item { + Column { + Card( + modifier = Modifier + .size(AgePosterSize) + .padding(end = ImageCardRowCardPadding), + onClick = { + viewModel.latestUpdate() + } + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = "继续加载") - } + Text(text = "继续加载") } - Spacer(modifier = Modifier.height(contentHeight)) } + 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/ui/features/home/main/MainScreen.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/main/MainScreen.kt index 9252708..5ff65e1 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/main/MainScreen.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/main/MainScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -51,7 +52,7 @@ fun MainScreen( val screenHeight = configuration.screenHeightDp.dp val screenWidth = configuration.screenWidthDp.dp - val homeData by remember { viewModel.homeDataState } + val homeData by viewModel.homeDataSF.collectAsState() var title by remember { mutableStateOf("") } var subTitle by remember { mutableStateOf(null) } @@ -174,12 +175,12 @@ fun MainScreen( } else { EmptyDataScreen() } - } else if (homeData.type == LazyType.FAILURE) { + } else if (homeData.type == LazyType.LOADING) { + LoadingScreen() + } else { ErrorScreen { - viewModel.fetchHome() + viewModel.refreshHomeData() } - } else { - LoadingScreen() } } diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/rank/RankScreen.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/rank/RankScreen.kt index 9541662..922e6e5 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/rank/RankScreen.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/rank/RankScreen.kt @@ -7,13 +7,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign @@ -34,8 +33,9 @@ import com.muedsa.compose.tv.widget.ErrorScreen import com.muedsa.compose.tv.widget.ExposedDropdownMenuButton import com.muedsa.compose.tv.widget.LoadingScreen import com.muedsa.uitl.LogUtil +import kotlinx.coroutines.flow.update -@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalTvMaterial3Api::class) @Composable fun RankScreen( viewModel: RankViewModel = hiltViewModel(), @@ -43,8 +43,8 @@ fun RankScreen( onNavigate: (NavigationItems, List?) -> Unit = { _, _ -> } ) { - var selectYear by remember { viewModel.selectedYear } - val rankLD by remember { viewModel.rankLDState } + val selectYear by viewModel.selectedYearSF.collectAsState() + val rankLD by viewModel.rankLDSF.collectAsState() LaunchedEffect(key1 = rankLD) { if (rankLD.type == LazyType.FAILURE) { @@ -69,8 +69,9 @@ fun RankScreen( item.text }, onSelected = { _, item -> - selectYear = item - viewModel.fetchRank() + viewModel.selectedYearSF.update { + item + } } ) } @@ -168,12 +169,8 @@ fun RankScreen( LoadingScreen() } else { ErrorScreen { - viewModel.fetchRank() + viewModel.selectedYearSF.value = selectYear } } } - - LaunchedEffect(key1 = Unit) { - viewModel.fetchRank() - } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/recommend/RecommendScreen.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/recommend/RecommendScreen.kt index 66fa22b..9b85d0a 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/recommend/RecommendScreen.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/recommend/RecommendScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi @@ -29,7 +30,9 @@ 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.ErrorScreen import com.muedsa.compose.tv.widget.ImageContentCard +import com.muedsa.compose.tv.widget.LoadingScreen import com.muedsa.compose.tv.widget.ScreenBackgroundState import com.muedsa.compose.tv.widget.ScreenBackgroundType import com.muedsa.uitl.LogUtil @@ -44,7 +47,7 @@ fun RecommendScreen( onNavigate: (NavigationItems, List?) -> Unit = { _, _ -> } ) { - val recommendLD by remember { viewModel.recommendLDState } + val recommendLD by viewModel.recommendLDSF.collectAsState() LaunchedEffect(key1 = recommendLD) { if (recommendLD.type == LazyType.FAILURE) { @@ -52,68 +55,72 @@ fun RecommendScreen( } } - Column(modifier = Modifier.padding(start = ScreenPaddingLeft)) { - Text( - text = "推荐列表", - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.titleLarge - ) + if (recommendLD.type == LazyType.SUCCESS) { + Column(modifier = Modifier.padding(start = ScreenPaddingLeft)) { + Text( + text = "推荐列表", + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.titleLarge + ) - if (!recommendLD.data.isNullOrEmpty()) { - val recommendList = recommendLD.data!! - val gridFocusRequester = remember { FocusRequester() } + if (!recommendLD.data.isNullOrEmpty()) { + val recommendList = recommendLD.data!! + 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 + 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 = recommendList, - key = { _, item -> item.aid } - ) { _, item -> - ImageContentCard( - modifier = Modifier.padding(end = ImageCardRowCardPadding), - 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())) - } + columns = TvGridCells.Adaptive(AgePosterSize.width + ImageCardRowCardPadding), + contentPadding = PaddingValues( + top = ImageCardRowCardPadding, + bottom = ImageCardRowCardPadding ) + ) { + itemsIndexed( + items = recommendList, + key = { _, item -> item.aid } + ) { _, item -> + ImageContentCard( + modifier = Modifier.padding(end = ImageCardRowCardPadding), + 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) { - viewModel.fetchRecommend() + } else if (recommendLD.type == LazyType.FAILURE) { + ErrorScreen { + viewModel.refreshRecommend() + } + } else { + LoadingScreen() } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/search/SearchScreen.kt b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/search/SearchScreen.kt index 3033797..243e018 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/search/SearchScreen.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/ui/features/home/search/SearchScreen.kt @@ -19,9 +19,9 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -75,9 +75,8 @@ fun SearchScreen( CardContentPadding * 2 } - var searchText by remember { viewModel.searchTextState } - - var searchAnimeLP by remember { viewModel.searchAnimeLPState } + val searchText by viewModel.searchTextSF.collectAsState() + val searchAnimeLP by viewModel.searchAnimeLPSF.collectAsState() LaunchedEffect(key1 = searchAnimeLP) { if (searchAnimeLP.type == LazyType.FAILURE) { @@ -111,14 +110,13 @@ fun SearchScreen( ), value = searchText, onValueChange = { - searchText = it + viewModel.searchTextSF.value = it }, singleLine = true ) Spacer(modifier = Modifier.width(16.dp)) OutlinedIconButton(onClick = { - searchAnimeLP = LazyPagedList.new() - viewModel.fetchSearchAnime() + viewModel.searchAnime(LazyPagedList.new(searchText)) }) { Icon( modifier = Modifier.size(ButtonDefaults.IconSize), @@ -191,35 +189,24 @@ fun SearchScreen( if (searchAnimeLP.type != LazyType.LOADING && searchAnimeLP.hasNext) { item { - Card( - modifier = Modifier - .size(AgePosterSize) - .padding(end = ImageCardRowCardPadding), - onClick = { - if (searchText.isNotEmpty()) { - viewModel.fetchSearchAnime() + Column { + Card( + modifier = Modifier + .size(AgePosterSize) + .padding(end = ImageCardRowCardPadding), + onClick = { + viewModel.searchAnime(searchAnimeLP) } - } - ) { - Column { - Card( - modifier = Modifier - .size(AgePosterSize) - .padding(end = ImageCardRowCardPadding), - onClick = { - viewModel.fetchSearchAnime() - } + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = "继续加载") - } + Text(text = "继续加载") } - Spacer(modifier = Modifier.height(contentHeight)) } + Spacer(modifier = Modifier.height(contentHeight)) } } } diff --git a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/AnimeDetailViewModel.kt b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/AnimeDetailViewModel.kt index f2f8bcb..68d517e 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/AnimeDetailViewModel.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/AnimeDetailViewModel.kt @@ -1,22 +1,22 @@ package com.muedsa.agetv.viewmodel -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.muedsa.agetv.exception.DataRequestException import com.muedsa.agetv.model.AgePlayInfoModel import com.muedsa.agetv.model.LazyData -import com.muedsa.agetv.model.LazyPagedList import com.muedsa.agetv.model.LazyType import com.muedsa.agetv.model.age.AnimeDetailPageModel -import com.muedsa.agetv.model.age.CommentModel import com.muedsa.agetv.model.dandanplay.DanAnimeInfo import com.muedsa.agetv.model.dandanplay.DanSearchAnime import com.muedsa.agetv.repository.AppRepository import com.muedsa.uitl.LogUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -27,51 +27,69 @@ class AnimeDetailViewModel @Inject constructor( private val repo: AppRepository ) : ViewModel() { - val animeIdLD = savedStateHandle.getLiveData(ANIME_ID_SAVED_STATE_KEY, "0") - val animeDetailLDState = mutableStateOf(LazyData.init()) - val commentsLPState = mutableStateOf(LazyPagedList.new()) + private val _navAnimeIdFlow = savedStateHandle.getStateFlow(ANIME_ID_SAVED_STATE_KEY, "0") + val animeIdSF = MutableStateFlow(_navAnimeIdFlow.value) - val danSearchAnimeListLDState = mutableStateOf>>(LazyData.init()) - val danAnimeInfoLDState = mutableStateOf>(LazyData.init()) + private val _animeDetailLDSF = MutableStateFlow(LazyData.init()) + val animeDetailLDSF: StateFlow> = _animeDetailLDSF - private fun fetchAnimeDetail(aid: Int) { - viewModelScope.launch(context = Dispatchers.IO) { - try { - repo.detail(aid).let { - animeDetailLDState.value = LazyData.success(it) - } - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - animeDetailLDState.value = LazyData.fail(t) - } - LogUtil.fb(t) + private val _danSearchAnimeListLDSF = MutableStateFlow(LazyData.init>()) + val danSearchAnimeListLDSF: StateFlow>> = _danSearchAnimeListLDSF + + private val _danAnimeInfoLDSF = MutableStateFlow(LazyData.init()) + val danAnimeInfoLDSF: StateFlow> = _danAnimeInfoLDSF + + + private fun animeDetail(aid: Int) { + viewModelScope.launch { + _animeDetailLDSF.value = LazyData.init() + _animeDetailLDSF.value = withContext(Dispatchers.IO) { + fetchAnimeDetail(aid) } } } - fun fetchNextPageComments() { - viewModelScope.launch(context = Dispatchers.IO) { - try { - val paged = commentsLPState.value - if (animeIdLD.value != null && paged.type != LazyType.LOADING && (paged.page == 0 || paged.hasNext)) { - repo.comment(aid = animeIdLD.value!!.toInt(), page = paged.nextPage).let { - if (it.code == 0) { - if (it.data != null) { - commentsLPState.value = paged - .successNext(it.data.comments, it.data.pagination.totalPage) - } - } else { - commentsLPState.value = - paged.failNext(RuntimeException(it.message)) - } - } - } - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - commentsLPState.value = commentsLPState.value.failNext(t) - } - LogUtil.fb(t) + fun danBangumi(animeId: Int) { + viewModelScope.launch { + _danAnimeInfoLDSF.value = LazyData.init() + _danAnimeInfoLDSF.value = withContext(Dispatchers.IO) { + fetchDanBangumi(animeId) + } + } + } + + private suspend fun fetchAnimeDetail(aid: Int): LazyData { + return try { + LazyData.success(repo.detail(aid)) + } catch (t: Throwable) { + LogUtil.fb(t) + LazyData.fail(t) + } + } + + private suspend fun fetchSearchDanAnime(title: String): LazyData> { + return try { + val resp = repo.danDanPlaySearchAnime(title) + if (resp.errorCode != SUCCESS_CODE) { + throw DataRequestException(resp.errorMessage) } + LazyData.success(resp.animes) + } catch (t: Throwable) { + LogUtil.fb(t) + LazyData.fail(t) + } + } + + private suspend fun fetchDanBangumi(animeId: Int): LazyData { + return try { + val resp = repo.danDanPlayGetAnime(animeId) + if (resp.errorCode != SUCCESS_CODE) { + throw DataRequestException(resp.errorMessage) + } + LazyData.success(resp.bangumi) + } catch (t: Throwable) { + LogUtil.fb(t) + LazyData.fail(t) } } @@ -93,64 +111,41 @@ class AnimeDetailViewModel @Inject constructor( } LogUtil.fb(t) } - } - } - fun searchDanAnime() { - if (animeDetailLDState.value.type == LazyType.SUCCESS - || animeDetailLDState.value.data != null - ) { - val title = animeDetailLDState.value.data!!.video.name - viewModelScope.launch(context = Dispatchers.IO) { - try { - repo.danDanPlaySearchAnime(title).let { - if (it.errorCode == SUCCESS_CODE) { - danSearchAnimeListLDState.value = LazyData.success(it.animes) - if (it.animes.isNotEmpty()) { - fetchDanBangumi(it.animes[0].animeId) - } - } else { - throw DataRequestException(it.errorMessage) - } - } - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - danSearchAnimeListLDState.value = LazyData.fail(t) - } - LogUtil.fb(t) - } + init { + viewModelScope.launch { + _navAnimeIdFlow.collectLatest { navAnimeId -> + animeIdSF.value = navAnimeId } } - } - fun fetchDanBangumi(animeId: Int) { - viewModelScope.launch(context = Dispatchers.IO) { - try { - repo.danDanPlayGetAnime(animeId).let { - if (it.errorCode == SUCCESS_CODE) { - danAnimeInfoLDState.value = LazyData.success(it.bangumi) - } else { - throw DataRequestException(it.errorMessage) + viewModelScope.launch { + animeIdSF.collectLatest { + val aid = it.toInt() + animeDetail(aid) + } + } + + viewModelScope.launch { + _animeDetailLDSF.collectLatest { + if (it.type == LazyType.SUCCESS) { + val title = it.data?.video?.name + if (!title.isNullOrBlank()) { + _danSearchAnimeListLDSF.value = LazyData.init() + _danSearchAnimeListLDSF.value = withContext(Dispatchers.IO) { + fetchSearchDanAnime(title) + } } } - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - danAnimeInfoLDState.value = LazyData.fail(t) - } - LogUtil.fb(t) } } - } - init { viewModelScope.launch { - animeIdLD.observeForever { - it?.let { - fetchAnimeDetail(it.toInt()) - // commentsLPState.value = LazyPagedList.new() - danSearchAnimeListLDState.value = LazyData.init() + _danSearchAnimeListLDSF.collectLatest { + if (it.type == LazyType.SUCCESS && !it.data.isNullOrEmpty()) { + danBangumi(it.data[0].animeId) } } } diff --git a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/CatalogViewModel.kt b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/CatalogViewModel.kt index d378435..537245e 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/CatalogViewModel.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/CatalogViewModel.kt @@ -1,15 +1,18 @@ package com.muedsa.agetv.viewmodel -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.muedsa.agetv.model.CatalogOptionsUIModel import com.muedsa.agetv.model.LazyPagedList -import com.muedsa.agetv.model.age.AgeCatalogOption import com.muedsa.agetv.model.age.CatalogAnimeModel import com.muedsa.agetv.repository.AppRepository import com.muedsa.uitl.LogUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -20,76 +23,57 @@ class CatalogViewModel @Inject constructor( private val repo: AppRepository ) : ViewModel() { - val orderState = mutableStateOf(AgeCatalogOption.Order[0]) - val regionState = mutableStateOf(AgeCatalogOption.Regions[0]) - val genreState = mutableStateOf(AgeCatalogOption.Genres[0]) - val yearState = mutableStateOf(AgeCatalogOption.Years[0]) - val seasonState = mutableStateOf(AgeCatalogOption.Seasons[0]) - val statusState = mutableStateOf(AgeCatalogOption.Status[0]) - val labelState = mutableStateOf(AgeCatalogOption.Labels[0]) - val resourceState = mutableStateOf(AgeCatalogOption.Resources[0]) + val querySF = MutableStateFlow(CatalogOptionsUIModel()) - val animeLPState = mutableStateOf(LazyPagedList.new()) + private val _animeLPSF = + MutableStateFlow(LazyPagedList.new(querySF.value)) + val animeLPSF: StateFlow> = _animeLPSF - fun fetchAnimeCatalog() { - animeLPState.value = animeLPState.value.loadingNext() - viewModelScope.launch(context = Dispatchers.IO) { - try { - repo.catalog( - genre = genreState.value.value, - label = labelState.value.value, - order = orderState.value.value, - region = regionState.value.value, - resource = regionState.value.value, - season = seasonState.value.value, - status = statusState.value.value, - year = yearState.value.value, - page = animeLPState.value.nextPage, - size = PAGE_SIZE - ).let { - animeLPState.value = animeLPState.value.successNext( - it.videos, - ceil(it.total.toDouble() / PAGE_SIZE).toInt() - ) - } - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - animeLPState.value = animeLPState.value.failNext(t) - } - LogUtil.fb(t) + fun catalog(lp: LazyPagedList) { + viewModelScope.launch { + val loadingLP = lp.loadingNext() + _animeLPSF.value = loadingLP + _animeLPSF.value = withContext(Dispatchers.IO) { + fetchCatalog(loadingLP) } } } + private suspend fun fetchCatalog( + lp: LazyPagedList + ): LazyPagedList { + return try { + repo.catalog( + genre = lp.query.genre.value, + label = lp.query.label.value, + order = lp.query.order.value, + region = lp.query.region.value, + resource = lp.query.resource.value, + season = lp.query.season.value, + status = lp.query.status.value, + year = lp.query.year.value, + page = lp.nextPage, + size = PAGE_SIZE + ).let { + lp.successNext(it.videos, ceil(it.total.toDouble() / PAGE_SIZE).toInt()) + } + } catch (t: Throwable) { + LogUtil.fb(t) + lp.failNext(t) + } + } + fun resetCatalogOptions() { - orderState.value = AgeCatalogOption.Order[0] - regionState.value = AgeCatalogOption.Regions[0] - genreState.value = AgeCatalogOption.Genres[0] - yearState.value = AgeCatalogOption.Years[0] - seasonState.value = AgeCatalogOption.Seasons[0] - statusState.value = AgeCatalogOption.Status[0] - labelState.value = AgeCatalogOption.Labels[0] - resourceState.value = AgeCatalogOption.Resources[0] + querySF.update { it.default() } } -// init { -// viewModelScope.launch { -// listOf( -// orderState, -// regionState, -// genreState, -// yearState, -// seasonState, -// statusState, -// labelState, -// resourceState -// ).forEach { -// snapshotFlow { it }.collect { -// animeLPState.value = animeLPState.value.needNew() -// } -// } -// } -// } + init { + viewModelScope.launch { + querySF.collectLatest { + catalog(LazyPagedList.new(it)) + } + } + } companion object { const val PAGE_SIZE = 20 diff --git a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/HomePageViewModel.kt b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/HomePageViewModel.kt index 3d80dc2..9154cd6 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/HomePageViewModel.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/HomePageViewModel.kt @@ -1,6 +1,5 @@ package com.muedsa.agetv.viewmodel -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.muedsa.agetv.model.LazyData @@ -9,6 +8,8 @@ import com.muedsa.agetv.repository.AppRepository import com.muedsa.uitl.LogUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -18,21 +19,27 @@ class HomePageViewModel @Inject constructor( private val repo: AppRepository ) : ViewModel() { - val homeDataState = mutableStateOf>(LazyData.init()) + private val _homeDataSF = MutableStateFlow(LazyData.init()) + val homeDataSF: StateFlow> = _homeDataSF - fun fetchHome() { - homeDataState.value = LazyData.init() - viewModelScope.launch(context = Dispatchers.IO) { - try { - repo.home().let { - homeDataState.value = LazyData.success(it) - } - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - homeDataState.value = LazyData.fail(t) - } - LogUtil.fb(t) + fun refreshHomeData() { + viewModelScope.launch { + _homeDataSF.value = withContext(Dispatchers.IO) { + fetchHomeData() } } } + + private suspend fun fetchHomeData(): LazyData { + return try { + LazyData.success(repo.home()) + } catch (t: Throwable) { + LogUtil.fb(t) + LazyData.fail(t) + } + } + + init { + refreshHomeData() + } } \ 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 index 783979a..56e0a52 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/LatestUpdateViewModel.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/LatestUpdateViewModel.kt @@ -1,6 +1,5 @@ package com.muedsa.agetv.viewmodel -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.muedsa.agetv.model.LazyPagedList @@ -9,6 +8,8 @@ import com.muedsa.agetv.repository.AppRepository import com.muedsa.uitl.LogUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -19,28 +20,41 @@ 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) + private val _latestUpdateLPSF = + MutableStateFlow(LazyPagedList.new(Unit)) + val latestUpdateLPSF: StateFlow> = _latestUpdateLPSF + + fun latestUpdate() { + viewModelScope.launch { + val lp = _latestUpdateLPSF.value.loadingNext() + _latestUpdateLPSF.value = lp + _latestUpdateLPSF.value = withContext(Dispatchers.IO) { + fetchLatestUpdate(lp) + } + } + + } + + private suspend fun fetchLatestUpdate( + lp: LazyPagedList + ): LazyPagedList { + return try { + return repo.update(lp.nextPage, PAGE_SIZE).let { + lp.successNext( + it.videos, + ceil(it.total.toDouble() / CatalogViewModel.PAGE_SIZE).toInt() + ) } + } catch (t: Throwable) { + LogUtil.fb(t) + lp.failNext(t) } } + init { + latestUpdate() + } + companion object { const val PAGE_SIZE = 30 } diff --git a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/RankViewModel.kt b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/RankViewModel.kt index abe5ef4..51faf45 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/RankViewModel.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/RankViewModel.kt @@ -1,6 +1,5 @@ package com.muedsa.agetv.viewmodel -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.muedsa.agetv.model.LazyData @@ -10,6 +9,9 @@ import com.muedsa.agetv.repository.AppRepository import com.muedsa.uitl.LogUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -18,22 +20,27 @@ import javax.inject.Inject class RankViewModel @Inject constructor( private val repo: AppRepository ) : ViewModel() { + val selectedYearSF = MutableStateFlow(AgeCatalogOption.Years[0]) - val selectedYear = mutableStateOf(AgeCatalogOption.Years[0]) + private val _rankLDSF = MutableStateFlow(LazyData.init>>()) + val rankLDSF: StateFlow>>> = _rankLDSF - val rankLDState = mutableStateOf>>>(LazyData.init()) + private suspend fun fetchRank(yearOption: AgeCatalogOption): LazyData>> { + return try { + LazyData.success(repo.rank(year = yearOption.value).rank) + } catch (t: Throwable) { + LogUtil.fb(t) + LazyData.fail(t) + } + } - fun fetchRank() { - rankLDState.value = LazyData.init() - viewModelScope.launch(context = Dispatchers.IO) { - try { - rankLDState.value = - LazyData.success(repo.rank(year = selectedYear.value.value).rank) - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - rankLDState.value = LazyData.fail(t) + init { + viewModelScope.launch { + selectedYearSF.collectLatest { + _rankLDSF.value = LazyData.init() + _rankLDSF.value = withContext(Dispatchers.IO) { + fetchRank(it) } - LogUtil.fb(t) } } } diff --git a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/RecommendViewModel.kt b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/RecommendViewModel.kt index a657af0..bc5fb1d 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/RecommendViewModel.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/RecommendViewModel.kt @@ -1,6 +1,5 @@ package com.muedsa.agetv.viewmodel -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.muedsa.agetv.model.LazyData @@ -9,6 +8,8 @@ import com.muedsa.agetv.repository.AppRepository import com.muedsa.uitl.LogUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -18,21 +19,28 @@ class RecommendViewModel @Inject constructor( private val repo: AppRepository ) : ViewModel() { - val recommendLDState = mutableStateOf>>(LazyData.init()) + private val _recommendLDSF = MutableStateFlow(LazyData.init>()) + val recommendLDSF: StateFlow>> = _recommendLDSF - fun fetchRecommend() { - recommendLDState.value = LazyData.init() - viewModelScope.launch(context = Dispatchers.IO) { - try { - repo.recommend().let { - recommendLDState.value = LazyData.success(it.videos) - } - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - recommendLDState.value = LazyData.fail(t) - } - LogUtil.fb(t) + fun refreshRecommend() { + viewModelScope.launch { + _recommendLDSF.value = LazyData.init() + _recommendLDSF.value = withContext(Dispatchers.IO) { + fetchRecommend() } } } + + private suspend fun fetchRecommend(): LazyData> { + return try { + LazyData.success(repo.recommend().videos) + } catch (t: Throwable) { + LogUtil.fb(t) + LazyData.fail(t) + } + } + + init { + refreshRecommend() + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/SearchViewModel.kt b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/SearchViewModel.kt index b0a7e04..2017578 100644 --- a/app/src/main/kotlin/com/muedsa/agetv/viewmodel/SearchViewModel.kt +++ b/app/src/main/kotlin/com/muedsa/agetv/viewmodel/SearchViewModel.kt @@ -1,6 +1,5 @@ package com.muedsa.agetv.viewmodel -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.muedsa.agetv.exception.DataRequestException @@ -11,6 +10,8 @@ import com.muedsa.agetv.service.AgePlayerService import com.muedsa.uitl.LogUtil import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -20,35 +21,43 @@ class SearchViewModel @Inject constructor( private val repo: AppRepository ) : ViewModel() { - val searchTextState = mutableStateOf("") + val searchTextSF = MutableStateFlow("") + private val _searchAnimeLPSF = + MutableStateFlow(LazyPagedList.new(searchTextSF.value)) + val searchAnimeLPSF: StateFlow> = _searchAnimeLPSF + + fun searchAnime(lp: LazyPagedList) { + if (lp.query.isNotBlank()) { + viewModelScope.launch { + val loadingLP = lp.loadingNext() + _searchAnimeLPSF.value = loadingLP + _searchAnimeLPSF.value = withContext(Dispatchers.IO) { + fetchSearch(loadingLP) + } + } + } + } - val searchAnimeLPState = mutableStateOf(LazyPagedList.new()) - fun fetchSearchAnime() { - searchAnimeLPState.value = searchAnimeLPState.value.loadingNext() - viewModelScope.launch(context = Dispatchers.IO) { - try { - repo.search( - query = searchTextState.value, - page = searchAnimeLPState.value.nextPage - ).let { - if (it.code == AgePlayerService.SUCCESS_CODE) { - if (it.data != null) { - searchAnimeLPState.value = searchAnimeLPState.value.successNext( - it.data.videos, - it.data.totalPage - ) - } - } else { - throw DataRequestException(it.message ?: "age request error"); - } + private suspend fun fetchSearch( + lp: LazyPagedList + ): LazyPagedList { + return try { + repo.search( + query = lp.query, + page = lp.nextPage + ).let { + if (it.code != AgePlayerService.SUCCESS_CODE) { + throw DataRequestException(it.message ?: "age request error") } - } catch (t: Throwable) { - withContext(Dispatchers.Main) { - searchAnimeLPState.value = searchAnimeLPState.value.failNext(t) + if (it.data == null) { + throw DataRequestException("response data is null") } - LogUtil.fb(t) + lp.successNext(it.data.videos, it.data.totalPage) } + } catch (t: Throwable) { + LogUtil.fb(t) + lp.failNext(t) } } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ddb17d..8de4c02 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ okhttp3-logging = "4.12.0" core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "core-splashscreen" } lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } +lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }