diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/photoview/PhotoViewActivity.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/photoview/PhotoViewActivity.kt index 850a38a6..260cfb33 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/photoview/PhotoViewActivity.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/photoview/PhotoViewActivity.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.rounded.Download import androidx.compose.material.icons.rounded.Share 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 @@ -46,6 +47,7 @@ import com.github.panpf.sketch.zoom.SketchZoomImageView import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.arch.BaseComposeActivityWithParcelable import com.huanchengfly.tieba.post.arch.collectPartialAsState +import com.huanchengfly.tieba.post.models.protos.LoadPicPageData import com.huanchengfly.tieba.post.models.protos.PhotoViewData import com.huanchengfly.tieba.post.toastShort import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme @@ -165,7 +167,11 @@ class PhotoViewActivity : BaseComposeActivityWithParcelable() { prop1 = PhotoViewUiState::hasNext, initial = false ) - val pageCount = items.size + val loadPicPageData by viewModel.uiState.collectPartialAsState( + prop1 = PhotoViewUiState::loadPicPageData, + initial = LoadPicPageData() + ) + val pageCount by remember { derivedStateOf { items.size } } Surface(color = Color.Black) { if (items.isNotEmpty()) { val coroutineScope = rememberCoroutineScope() @@ -173,12 +179,34 @@ class PhotoViewActivity : BaseComposeActivityWithParcelable() { LaunchedEffect(initialIndex) { if (pagerState.currentPage != initialIndex) pagerState.scrollToPage(initialIndex) } + LaunchedEffect(pagerState.currentPage, pageCount, loadPicPageData) { + loadPicPageData?.let { + val item = items[pagerState.currentPage] + if (pagerState.currentPage == 0 && hasPrev) { + viewModel.send( + PhotoViewUiIntent.LoadPrev( + item.picId, + item.overallIndex, + it + ) + ) + } else if (pagerState.currentPage == pageCount - 1 && hasNext) { + viewModel.send( + PhotoViewUiIntent.LoadMore( + item.picId, + item.overallIndex, + it + ) + ) + } + } + } Box(modifier = Modifier.fillMaxSize()) { HorizontalPager( pageCount = pageCount, state = pagerState, key = { - "${items[it].originUrl}_${items[it].overallIndex}" + "${items[it].overallIndex}" } ) { val item = items[it] @@ -186,9 +214,10 @@ class PhotoViewActivity : BaseComposeActivityWithParcelable() { imageUri = item.originUrl, modifier = Modifier.fillMaxSize(), onDrag = { dx, dy, isAtEdge -> + val currentPage = pagerState.currentPage if (abs(dy) < 15 && abs(dx) > 25 && isAtEdge) { - val prevPage = it - 1 - val nextPage = it + 1 + val prevPage = currentPage - 1 + val nextPage = currentPage + 1 if (dx > 0 && prevPage >= 0) { coroutineScope.launch { pagerState.animateScrollToPage(prevPage) @@ -225,7 +254,7 @@ class PhotoViewActivity : BaseComposeActivityWithParcelable() { ) { val index = pagerState.currentPage if (totalAmount > 1) { - val picIndex = items[index].overallIndex ?: (index + 1) + val picIndex = items[index].overallIndex Text( text = "$picIndex / $totalAmount", modifier = Modifier.weight(1f) diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/photoview/PhotoViewViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/photoview/PhotoViewViewModel.kt index 979da66c..22aa823c 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/photoview/PhotoViewViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/photoview/PhotoViewViewModel.kt @@ -8,6 +8,7 @@ import com.huanchengfly.tieba.post.arch.PartialChangeProducer import com.huanchengfly.tieba.post.arch.UiEvent import com.huanchengfly.tieba.post.arch.UiIntent import com.huanchengfly.tieba.post.arch.UiState +import com.huanchengfly.tieba.post.models.protos.LoadPicPageData import com.huanchengfly.tieba.post.models.protos.PhotoViewData import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow @@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart class PhotoViewViewModel : BaseViewModel() { @@ -28,24 +30,95 @@ class PhotoViewViewModel : @OptIn(FlowPreview::class) override fun toPartialChangeFlow(intentFlow: Flow): Flow = merge( - intentFlow.filterIsInstance().flatMapConcat { it.producePartialChange() } + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, ) + private fun PhotoViewUiIntent.LoadPrev.producePartialChange(): Flow = + TiebaApi.getInstance() + .picPageFlow( + forumId = data.forumId.toString(), + forumName = data.forumName, + threadId = data.threadId.toString(), + seeLz = data.seeLz, + picId = picId, + picIndex = overallIndex.toString(), + objType = data.objType, + prev = true + ) + .map { picPageBean -> + val items = picPageBean.picList.map { + PhotoViewItem( + picId = it.img.original.id, + originUrl = it.img.original.originalSrc, + url = if (it.showOriginalBtn) it.img.original.bigCdnSrc else null, + overallIndex = it.overAllIndex.toInt() + ) + } + val hasPrev = items.first().overallIndex > 1 + PhotoViewPartialChange.LoadPrev.Success( + hasPrev = hasPrev, + items = items + ) + } + .onStart { emit(PhotoViewPartialChange.LoadPrev.Start) } + .catch { + emit(PhotoViewPartialChange.LoadPrev.Failure(it)) + } + + private fun PhotoViewUiIntent.LoadMore.producePartialChange(): Flow = + TiebaApi.getInstance() + .picPageFlow( + forumId = data.forumId.toString(), + forumName = data.forumName, + threadId = data.threadId.toString(), + seeLz = data.seeLz, + picId = picId, + picIndex = overallIndex.toString(), + objType = data.objType, + prev = false + ) + .map { picPageBean -> + val items = picPageBean.picList.map { + PhotoViewItem( + picId = it.img.original.id, + originUrl = it.img.original.originalSrc, + url = if (it.showOriginalBtn) it.img.original.bigCdnSrc else null, + overallIndex = it.overAllIndex.toInt() + ) + } + val hasNext = items.last().overallIndex < picPageBean.picAmount.toInt() + PhotoViewPartialChange.LoadMore.Success( + hasNext = hasNext, + items = items + ) + } + .onStart { emit(PhotoViewPartialChange.LoadMore.Start) } + .catch { + emit(PhotoViewPartialChange.LoadMore.Failure(it)) + } + private fun PhotoViewUiIntent.Init.producePartialChange(): Flow { val flow = if (data.data_ == null) { flowOf( PhotoViewPartialChange.Init.Success( - items = data.picItems.map { + items = data.picItems.mapIndexed { index, item -> PhotoViewItem( - originUrl = it.originUrl, - url = if (it.showOriginBtn) it.url else null, - overallIndex = null + picId = item.picId, + originUrl = item.originUrl, + url = if (item.showOriginBtn) item.url else null, + overallIndex = index + 1 ) }, hasNext = false, hasPrev = false, totalAmount = data.picItems.size, - initialIndex = data.index + initialIndex = data.index, + loadPicPageData = null ) ) } else { @@ -64,32 +137,35 @@ class PhotoViewViewModel : val picAmount = picPageBean.picAmount.toInt() val fetchedItems = picPageBean.picList.map { PhotoViewItem( + picId = it.img.original.id, originUrl = it.img.original.originalSrc, url = if (it.showOriginalBtn) it.img.original.bigCdnSrc else null, overallIndex = it.overAllIndex.toInt() ) } - val firstItemIndex = fetchedItems.first().overallIndex!! + val firstItemIndex = fetchedItems.first().overallIndex val localItems = if (data.data_.picIndex == 1) emptyList() else data.picItems.subList( 0, data.data_.picIndex - 1 ).mapIndexed { index, item -> PhotoViewItem( + picId = item.picId, originUrl = item.originUrl, url = if (item.showOriginBtn) item.url else null, overallIndex = firstItemIndex - (data.data_.picIndex - 1 - index) ) } val items = localItems + fetchedItems - val hasNext = items.last().overallIndex!! < picAmount - val hasPrev = items.first().overallIndex!! > 1 + val hasNext = items.last().overallIndex < picAmount + val hasPrev = items.first().overallIndex > 1 PhotoViewPartialChange.Init.Success( hasPrev = hasPrev, hasNext = hasNext, totalAmount = picAmount, items = items, - initialIndex = data.data_.picIndex - 1 + initialIndex = data.data_.picIndex - 1, + loadPicPageData = data.data_ ) } .catch { @@ -103,6 +179,18 @@ class PhotoViewViewModel : sealed interface PhotoViewUiIntent : UiIntent { data class Init(val data: PhotoViewData) : PhotoViewUiIntent + + data class LoadMore( + val picId: String, + val overallIndex: Int, + val data: LoadPicPageData + ) : PhotoViewUiIntent + + data class LoadPrev( + val picId: String, + val overallIndex: Int, + val data: LoadPicPageData + ) : PhotoViewUiIntent } sealed interface PhotoViewPartialChange : PartialChange { @@ -115,16 +203,18 @@ sealed interface PhotoViewPartialChange : PartialChange { hasPrev = hasPrev, totalAmount = totalAmount, initialIndex = initialIndex, + loadPicPageData = loadPicPageData, isLoading = false ) is Failure -> { oldState.copy( - data = data.picItems.map { + data = data.picItems.mapIndexed { index, item -> PhotoViewItem( - originUrl = it.originUrl, - url = if (it.showOriginBtn) it.url else null, - overallIndex = null + picId = item.picId, + originUrl = item.originUrl, + url = if (item.showOriginBtn) item.url else null, + overallIndex = index + 1 ) }, hasNext = false, @@ -142,6 +232,7 @@ sealed interface PhotoViewPartialChange : PartialChange { val hasPrev: Boolean, val totalAmount: Int, val initialIndex: Int, + val loadPicPageData: LoadPicPageData?, ) : Init() data class Failure( @@ -149,6 +240,58 @@ sealed interface PhotoViewPartialChange : PartialChange { val error: Throwable ) : Init() } + + sealed class LoadPrev : PhotoViewPartialChange { + override fun reduce(oldState: PhotoViewUiState): PhotoViewUiState = + when (this) { + Start -> oldState.copy(isLoading = true) + + is Success -> oldState.copy( + data = items.filterNot { item -> oldState.data.any { item.picId == it.picId } } + oldState.data, + hasPrev = hasPrev, + isLoading = false + ) + + is Failure -> oldState.copy(isLoading = false) + } + + object Start : LoadPrev() + + data class Success( + val items: List, + val hasPrev: Boolean, + ) : LoadPrev() + + data class Failure( + val error: Throwable + ) : LoadPrev() + } + + sealed class LoadMore : PhotoViewPartialChange { + override fun reduce(oldState: PhotoViewUiState): PhotoViewUiState = + when (this) { + Start -> oldState.copy(isLoading = true) + + is Success -> oldState.copy( + data = oldState.data + items.filterNot { item -> oldState.data.any { item.picId == it.picId } }, + hasNext = hasNext, + isLoading = false + ) + + is Failure -> oldState.copy(isLoading = false) + } + + object Start : LoadMore() + + data class Success( + val items: List, + val hasNext: Boolean, + ) : LoadMore() + + data class Failure( + val error: Throwable + ) : LoadMore() + } } data class PhotoViewUiState( @@ -158,12 +301,14 @@ data class PhotoViewUiState( val hasNext: Boolean = false, val hasPrev: Boolean = false, val initialIndex: Int = 0, + val loadPicPageData: LoadPicPageData? = null, ) : UiState sealed interface PhotoViewUiEvent : UiEvent data class PhotoViewItem( + val picId: String, val originUrl: String, val url: String?, - val overallIndex: Int? + val overallIndex: Int ) \ No newline at end of file