From a6fb5b6ea4b4cc37ea60a4c5b7a9704905e42fbb Mon Sep 17 00:00:00 2001 From: whitescent Date: Thu, 30 Nov 2023 09:50:48 +0800 Subject: [PATCH 1/5] feat: use my paginator instead of paging3 --- .../mastify/data/model/ui/StatusUiData.kt | 3 +- .../data/repository/ExploreRepository.kt | 29 --- .../mastify/domain/StatusActionHandler.kt | 4 +- .../mastify/mapper/status/StatusMapper.kt | 3 +- .../network/InstanceSwitchAuthInterceptor.kt | 1 - .../whitescent/mastify/paging/Paginator.kt | 56 ++++- .../mastify/screen/explore/Explore.kt | 60 +++-- .../mastify/screen/explore/ExplorePager.kt | 27 +- .../whitescent/mastify/screen/home/Home.kt | 23 +- .../mastify/ui/component/AnimatedText.kt | 4 +- .../ui/component/status/StatusCommonList.kt | 114 ++++++++- .../ui/component/status/StatusListItem.kt | 12 +- .../component/status/action/FavoriteButton.kt | 4 - .../component/status/action/ReblogButton.kt | 24 +- .../mastify/viewModel/ExplorerViewModel.kt | 232 ++++++++++++++++-- .../mastify/viewModel/HomeViewModel.kt | 2 +- .../viewModel/StatusDetailViewModel.kt | 3 +- 17 files changed, 457 insertions(+), 144 deletions(-) diff --git a/app/src/main/java/com/github/whitescent/mastify/data/model/ui/StatusUiData.kt b/app/src/main/java/com/github/whitescent/mastify/data/model/ui/StatusUiData.kt index 4bebd3ea..f2fb7ddc 100644 --- a/app/src/main/java/com/github/whitescent/mastify/data/model/ui/StatusUiData.kt +++ b/app/src/main/java/com/github/whitescent/mastify/data/model/ui/StatusUiData.kt @@ -43,7 +43,6 @@ data class StatusUiData( val emojis: ImmutableList, val attachments: ImmutableList, val actionable: Status, - val actionableId: String, val reblogDisplayName: String, val fullname: String, val createdAt: String, @@ -57,6 +56,8 @@ data class StatusUiData( val hasUnloadedStatus: Boolean, ) { + val actionableId inline get() = reblog?.id ?: id + val parsedContent: String = Jsoup.parse(content).body().text() val isInReplyTo inline get() = inReplyToId != null diff --git a/app/src/main/java/com/github/whitescent/mastify/data/repository/ExploreRepository.kt b/app/src/main/java/com/github/whitescent/mastify/data/repository/ExploreRepository.kt index 3f4012f4..6c6c3e60 100644 --- a/app/src/main/java/com/github/whitescent/mastify/data/repository/ExploreRepository.kt +++ b/app/src/main/java/com/github/whitescent/mastify/data/repository/ExploreRepository.kt @@ -17,16 +17,9 @@ package com.github.whitescent.mastify.data.repository -import androidx.paging.Pager -import androidx.paging.PagingConfig -import androidx.paging.PagingData import at.connyduck.calladapter.networkresult.fold -import com.github.whitescent.mastify.data.model.ui.StatusUiData import com.github.whitescent.mastify.network.MastodonApi import com.github.whitescent.mastify.network.model.search.SearchResult -import com.github.whitescent.mastify.paging.PublicTimelinePagingSource -import com.github.whitescent.mastify.paging.TrendingPagingSource -import kotlinx.coroutines.flow.Flow import javax.inject.Inject import javax.inject.Singleton @@ -47,28 +40,6 @@ class ExploreRepository @Inject constructor( } ) } - - fun getTrendingStatusPager(): Flow> = - Pager( - config = PagingConfig( - pageSize = 20, - enablePlaceholders = false, - ), - pagingSourceFactory = { - TrendingPagingSource(api = api) - }, - ).flow - - fun getPublicTimelinePager(): Flow> = - Pager( - config = PagingConfig( - pageSize = 20, - enablePlaceholders = false, - ), - pagingSourceFactory = { - PublicTimelinePagingSource(api = api) - }, - ).flow } sealed interface SearchPreviewResult { diff --git a/app/src/main/java/com/github/whitescent/mastify/domain/StatusActionHandler.kt b/app/src/main/java/com/github/whitescent/mastify/domain/StatusActionHandler.kt index 32524cbb..8fb2919f 100644 --- a/app/src/main/java/com/github/whitescent/mastify/domain/StatusActionHandler.kt +++ b/app/src/main/java/com/github/whitescent/mastify/domain/StatusActionHandler.kt @@ -18,6 +18,7 @@ package com.github.whitescent.mastify.domain import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import com.github.whitescent.mastify.network.MastodonApi import com.github.whitescent.mastify.ui.component.status.StatusSnackbarType @@ -33,8 +34,7 @@ class StatusActionHandler(private val api: MastodonApi) { val snackBarFlow = snackBarChanel.receiveAsFlow() suspend fun onStatusAction(action: StatusAction, context: Context) { - val clipManager = - context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + val clipManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager when (action) { is StatusAction.Favorite -> { if (action.favorite) api.favouriteStatus(action.id) else api.unfavouriteStatus(action.id) diff --git a/app/src/main/java/com/github/whitescent/mastify/mapper/status/StatusMapper.kt b/app/src/main/java/com/github/whitescent/mastify/mapper/status/StatusMapper.kt index d32f85ae..8d315ffe 100644 --- a/app/src/main/java/com/github/whitescent/mastify/mapper/status/StatusMapper.kt +++ b/app/src/main/java/com/github/whitescent/mastify/mapper/status/StatusMapper.kt @@ -63,8 +63,9 @@ fun Status.toUiData(): StatusUiData = StatusUiData( favouritesCount = reblog?.favouritesCount ?: favouritesCount, favorited = reblog?.favorited ?: favorited, inReplyToId = reblog?.inReplyToId ?: inReplyToId, + // NOTE: make sure that you need to do a conversion whenever you use this property, + // otherwise it may cause inconsistencies between StatusUiData and StatusUiData.actionable actionable = actionableStatus, - actionableId = actionableStatus.id, hasUnloadedStatus = hasUnloadedStatus ) diff --git a/app/src/main/java/com/github/whitescent/mastify/network/InstanceSwitchAuthInterceptor.kt b/app/src/main/java/com/github/whitescent/mastify/network/InstanceSwitchAuthInterceptor.kt index 421a07fa..8e1e0a28 100644 --- a/app/src/main/java/com/github/whitescent/mastify/network/InstanceSwitchAuthInterceptor.kt +++ b/app/src/main/java/com/github/whitescent/mastify/network/InstanceSwitchAuthInterceptor.kt @@ -46,7 +46,6 @@ class InstanceSwitchAuthInterceptor(private val db: AppDatabase) : Interceptor { } else { runBlocking { val currentAccount = db.accountDao().getActiveAccount() - logcat { "currentAccount $currentAccount" } if (currentAccount != null) { val accessToken = currentAccount.accessToken if (accessToken.isNotEmpty()) { diff --git a/app/src/main/java/com/github/whitescent/mastify/paging/Paginator.kt b/app/src/main/java/com/github/whitescent/mastify/paging/Paginator.kt index 10203d30..0508545a 100644 --- a/app/src/main/java/com/github/whitescent/mastify/paging/Paginator.kt +++ b/app/src/main/java/com/github/whitescent/mastify/paging/Paginator.kt @@ -17,6 +17,16 @@ package com.github.whitescent.mastify.paging +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import com.github.whitescent.mastify.paging.LoadState.Error +import com.github.whitescent.mastify.paging.LoadState.NotLoading +import kotlinx.coroutines.launch + class Paginator( private val refreshKey: Key, private inline val getAppendKey: suspend () -> Key, @@ -25,7 +35,12 @@ class Paginator( private inline val onRequest: suspend (Key) -> Result>, private inline val onSuccess: suspend (loadState: LoadState, items: List) -> Unit, ) : PaginatorInterface { - private var loadState = LoadState.NotLoading + + var loadState = NotLoading + private set + + var endReached: Boolean = false + private set override suspend fun append() { if (loadState == LoadState.Append) return @@ -35,16 +50,17 @@ class Paginator( val appendKey = getAppendKey() val result = onRequest(appendKey).getOrElse { onError(it) - loadState = LoadState.Error + loadState = Error onLoadUpdated(loadState) return } + if (result.isEmpty()) endReached = true onSuccess(loadState, result) - loadState = LoadState.NotLoading + loadState = NotLoading onLoadUpdated(loadState) } catch (e: Exception) { onError(e) - loadState = LoadState.Error + loadState = Error onLoadUpdated(loadState) return } @@ -57,22 +73,48 @@ class Paginator( try { val result = onRequest(refreshKey).getOrElse { onError(it) - loadState = LoadState.Error + loadState = Error onLoadUpdated(loadState) return } + if (result.isEmpty()) endReached = true onSuccess(loadState, result) - loadState = LoadState.NotLoading + loadState = NotLoading onLoadUpdated(loadState) } catch (e: Exception) { onError(e) - loadState = LoadState.Error + loadState = Error onLoadUpdated(loadState) return } } } +@Composable +fun LaunchPaginatorListener( + lazyListState: LazyListState, + list: List, + paginator: Paginator<*, *>, + fetchNumber: Int, + threshold: Int = 10 +) { + val firstVisibleItemIndex by remember(lazyListState) { + derivedStateOf { + lazyListState.firstVisibleItemIndex + } + } + if (list.isNotEmpty()) { + if (!paginator.endReached && paginator.loadState == NotLoading && + firstVisibleItemIndex >= (list.size - ((list.size / fetchNumber) * threshold)) + ) { + val scope = rememberCoroutineScope() + scope.launch { + paginator.append() + } + } + } +} + enum class LoadState { Refresh, Append, Error, NotLoading } diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt b/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt index 0f302c87..4b1e7786 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt @@ -55,7 +55,6 @@ import androidx.compose.material3.Text 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.rememberCoroutineScope @@ -81,11 +80,11 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.paging.compose.collectAsLazyPagingItems import com.gigamole.composeshadowsplus.rsblur.rsBlurShadow import com.github.whitescent.R import com.github.whitescent.mastify.AppNavGraph import com.github.whitescent.mastify.network.model.search.SearchResult +import com.github.whitescent.mastify.paging.LaunchPaginatorListener import com.github.whitescent.mastify.screen.destinations.ProfileDestination import com.github.whitescent.mastify.screen.destinations.StatusDetailDestination import com.github.whitescent.mastify.screen.destinations.StatusMediaScreenDestination @@ -101,6 +100,7 @@ import com.github.whitescent.mastify.ui.transitions.BottomBarScreenTransitions import com.github.whitescent.mastify.utils.AppState import com.github.whitescent.mastify.viewModel.ExplorerKind import com.github.whitescent.mastify.viewModel.ExplorerViewModel +import com.github.whitescent.mastify.viewModel.ExplorerViewModel.Companion.EXPLOREPAGINGFETCHNUMBER import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch @@ -120,9 +120,10 @@ fun Explore( val uiState = viewModel.uiState val context = LocalContext.current - var selectedTab by remember { mutableIntStateOf(0) } - var hideContent by remember { mutableStateOf(false) } + val currentExploreKind by viewModel.currentExploreKind.collectAsStateWithLifecycle() + // when user focus on searchBar, we need hide content + var hideContent by remember { mutableStateOf(false) } val pagerState = rememberPagerState { ExplorerKind.entries.size } val focusRequester = remember { FocusRequester() } @@ -131,10 +132,10 @@ fun Explore( val snackbarState = rememberStatusSnackBarState() val trendingStatusListState = rememberLazyListState() - val trendingStatusList = viewModel.trendingStatusPager.collectAsLazyPagingItems() + val trendingStatusList by viewModel.trending.collectAsStateWithLifecycle() val publicTimelineListState = rememberLazyListState() - val publicTimelineList = viewModel.publicTimelinePager.collectAsLazyPagingItems() + val publicTimelineList by viewModel.publicTimeline.collectAsStateWithLifecycle() val searchingResult by viewModel.searchPreviewResult.collectAsStateWithLifecycle() @@ -203,14 +204,12 @@ fun Explore( else -> { Column { ExploreTabBar( - selectedTab = selectedTab, - modifier = Modifier - .padding(horizontal = 12.dp) - .fillMaxWidth() - ) { currentTab -> - selectedTab = currentTab + currentExploreKind = currentExploreKind, + modifier = Modifier.padding(horizontal = 12.dp).fillMaxWidth() + ) { kind -> + viewModel.syncExploreKind(kind) scope.launch { - pagerState.scrollToPage(currentTab) + pagerState.scrollToPage(kind) } } AppHorizontalDivider(thickness = 1.dp) @@ -220,8 +219,8 @@ fun Explore( trendingStatusList = trendingStatusList, publicTimelineListState = publicTimelineListState, publicTimelineList = publicTimelineList, - action = { action -> - viewModel.onStatusAction(action, context) + action = { action, kind, status -> + viewModel.onStatusAction(action, context, kind, status) }, navigateToDetail = { targetStatus -> navigator.navigate( @@ -231,6 +230,8 @@ fun Explore( ) ) }, + refreshKind = viewModel::refreshExploreKind, + append = viewModel::appendExploreKind, navigateToMedia = { attachments, targetIndex -> navigator.navigate( StatusMediaScreenDestination(attachments.toTypedArray(), targetIndex) @@ -244,7 +245,7 @@ fun Explore( ) LaunchedEffect(pagerState) { snapshotFlow { pagerState.currentPage }.collect { page -> - selectedTab = page + viewModel.syncExploreKind(page) } } } @@ -264,7 +265,7 @@ fun Explore( ) } - LaunchedEffect(Unit) { + LaunchedEffect(currentExploreKind) { launch { viewModel.snackBarFlow.collect { snackbarState.show(it) @@ -273,6 +274,7 @@ fun Explore( launch { appState.scrollToTopFlow.collect { trendingStatusListState.scrollToItem(0) + publicTimelineListState.scrollToItem(0) } } launch { @@ -281,6 +283,18 @@ fun Explore( } } } + LaunchPaginatorListener( + lazyListState = trendingStatusListState, + list = trendingStatusList.timeline, + paginator = viewModel.trendingPaginator, + fetchNumber = EXPLOREPAGINGFETCHNUMBER + ) + LaunchPaginatorListener( + lazyListState = publicTimelineListState, + list = publicTimelineList.timeline, + paginator = viewModel.publicTimelinePaginator, + fetchNumber = EXPLOREPAGINGFETCHNUMBER + ) } @Composable @@ -376,15 +390,15 @@ fun ExploreSearchBar( @OptIn(ExperimentalMaterial3Api::class) @Composable fun ExploreTabBar( - selectedTab: Int, + currentExploreKind: ExplorerKind, modifier: Modifier = Modifier, onTabClick: (Int) -> Unit ) { PrimaryTabRow( - selectedTabIndex = selectedTab, + selectedTabIndex = currentExploreKind.ordinal, indicator = { TabRowDefaults.PrimaryIndicator( - modifier = Modifier.tabIndicatorOffset(it[selectedTab]), + modifier = Modifier.tabIndicatorOffset(it[currentExploreKind.ordinal]), width = 40.dp, height = 5.dp, color = AppTheme.colors.accent @@ -394,8 +408,8 @@ fun ExploreTabBar( containerColor = Color.Transparent, modifier = modifier ) { - ExplorerKind.entries.forEachIndexed { index, tab -> - val selected = selectedTab == index + ExplorerKind.entries.forEachIndexed { index, kind -> + val selected = currentExploreKind == kind Tab( selected = selected, onClick = { @@ -406,7 +420,7 @@ fun ExploreTabBar( unselectedContentColor = Color.Transparent ) { Text( - text = stringResource(tab.stringRes), + text = stringResource(kind.stringRes), fontSize = 14.sp, fontWeight = FontWeight(700), color = if (selected) AppTheme.colors.primaryContent else AppTheme.colors.secondaryContent, diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExplorePager.kt b/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExplorePager.kt index 0029f7ee..3c6ea09c 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExplorePager.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExplorePager.kt @@ -24,12 +24,15 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.paging.compose.LazyPagingItems import com.github.whitescent.mastify.data.model.ui.StatusUiData import com.github.whitescent.mastify.network.model.account.Account import com.github.whitescent.mastify.network.model.status.Status import com.github.whitescent.mastify.ui.component.status.StatusCommonList import com.github.whitescent.mastify.utils.StatusAction +import com.github.whitescent.mastify.viewModel.ExplorerKind +import com.github.whitescent.mastify.viewModel.ExplorerKind.PublicTimeline +import com.github.whitescent.mastify.viewModel.ExplorerKind.Trending +import com.github.whitescent.mastify.viewModel.StatusCommonListData import kotlinx.collections.immutable.ImmutableList @OptIn(ExperimentalFoundationApi::class) @@ -37,11 +40,13 @@ import kotlinx.collections.immutable.ImmutableList fun ExplorePager( state: PagerState, trendingStatusListState: LazyListState, - trendingStatusList: LazyPagingItems, + trendingStatusList: StatusCommonListData, publicTimelineListState: LazyListState, - publicTimelineList: LazyPagingItems, + publicTimelineList: StatusCommonListData, modifier: Modifier = Modifier, - action: (StatusAction) -> Unit, + action: (StatusAction, ExplorerKind, Status) -> Unit, + refreshKind: (ExplorerKind) -> Unit, + append: (ExplorerKind) -> Unit, navigateToDetail: (Status) -> Unit, navigateToProfile: (Account) -> Unit, navigateToMedia: (ImmutableList, Int) -> Unit, @@ -51,25 +56,29 @@ fun ExplorePager( pageContent = { when (it) { 0 -> StatusCommonList( - statusList = trendingStatusList, + statusCommonListData = trendingStatusList, statusListState = trendingStatusListState, - action = action, + action = { action, status -> action(action, Trending, status) }, enablePullRefresh = true, + refreshList = { refreshKind(Trending) }, + append = { append(Trending) }, navigateToDetail = navigateToDetail, navigateToProfile = navigateToProfile, navigateToMedia = navigateToMedia, ) 1 -> StatusCommonList( - statusList = publicTimelineList, + statusCommonListData = publicTimelineList, statusListState = publicTimelineListState, - action = action, + action = { action, status -> action(action, PublicTimeline, status) }, enablePullRefresh = true, + refreshList = { refreshKind(PublicTimeline) }, + append = { append(PublicTimeline) }, navigateToDetail = navigateToDetail, navigateToProfile = navigateToProfile, navigateToMedia = navigateToMedia, ) } }, - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), ) } diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/home/Home.kt b/app/src/main/java/com/github/whitescent/mastify/screen/home/Home.kt index be335862..922a899c 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/home/Home.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/home/Home.kt @@ -86,6 +86,7 @@ import com.github.whitescent.mastify.data.repository.HomeRepository.Companion.PA import com.github.whitescent.mastify.database.model.AccountEntity import com.github.whitescent.mastify.mapper.status.getReplyChainType import com.github.whitescent.mastify.mapper.status.hasUnloadedParent +import com.github.whitescent.mastify.paging.LaunchPaginatorListener import com.github.whitescent.mastify.paging.LoadState import com.github.whitescent.mastify.paging.LoadState.Error import com.github.whitescent.mastify.paging.LoadState.NotLoading @@ -118,8 +119,6 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class, FlowPreview::class) @@ -301,18 +300,6 @@ fun Home( LaunchedEffect(firstVisibleIndex) { if (firstVisibleIndex == 0 && uiState.showNewStatusButton) viewModel.dismissButton() - launch { - snapshotFlow { firstVisibleIndex } - .filter { timeline.isNotEmpty() } - .map { - !uiState.endReached && uiState.timelineLoadState == NotLoading && - lazyState.firstVisibleItemIndex >= (timeline.size - ((timeline.size / FETCHNUMBER) * PAGINGTHRESHOLD)) - } - .filter { it } - .collect { - viewModel.append() - } - } launch { snapshotFlow { firstVisibleIndex } .debounce(500L) @@ -322,6 +309,14 @@ fun Home( } } } + + LaunchPaginatorListener( + lazyListState = lazyState, + list = timeline, + paginator = viewModel.paginator, + fetchNumber = FETCHNUMBER, + threshold = PAGINGTHRESHOLD + ) } @Composable diff --git a/app/src/main/java/com/github/whitescent/mastify/ui/component/AnimatedText.kt b/app/src/main/java/com/github/whitescent/mastify/ui/component/AnimatedText.kt index 4d59f386..cc38b29e 100644 --- a/app/src/main/java/com/github/whitescent/mastify/ui/component/AnimatedText.kt +++ b/app/src/main/java/com/github/whitescent/mastify/ui/component/AnimatedText.kt @@ -36,9 +36,7 @@ fun AnimatedText( text: String, modifier: Modifier = Modifier, color: Color = Color.Unspecified, - style: TextStyle = LocalTextStyle.current.copy( - color = color - ) + style: TextStyle = LocalTextStyle.current.copy(color = color) ) { AnimatedContent( targetState = text, diff --git a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusCommonList.kt b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusCommonList.kt index a4c92dc7..8a6e945f 100644 --- a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusCommonList.kt +++ b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusCommonList.kt @@ -23,6 +23,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh @@ -48,6 +51,10 @@ import com.github.whitescent.mastify.mapper.status.getReplyChainType import com.github.whitescent.mastify.mapper.status.hasUnloadedParent import com.github.whitescent.mastify.network.model.account.Account import com.github.whitescent.mastify.network.model.status.Status +import com.github.whitescent.mastify.network.model.status.Status.Attachment +import com.github.whitescent.mastify.paging.LoadState.Append +import com.github.whitescent.mastify.paging.LoadState.Error +import com.github.whitescent.mastify.paging.LoadState.NotLoading import com.github.whitescent.mastify.ui.component.AppHorizontalDivider import com.github.whitescent.mastify.ui.component.StatusAppendingIndicator import com.github.whitescent.mastify.ui.component.StatusEndIndicator @@ -56,6 +63,7 @@ import com.github.whitescent.mastify.ui.component.status.paging.PageType import com.github.whitescent.mastify.ui.component.status.paging.StatusListLoadError import com.github.whitescent.mastify.ui.component.status.paging.StatusListLoading import com.github.whitescent.mastify.utils.StatusAction +import com.github.whitescent.mastify.viewModel.StatusCommonListData import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -69,7 +77,7 @@ fun StatusCommonList( action: (StatusAction) -> Unit, navigateToDetail: (Status) -> Unit, navigateToProfile: (Account) -> Unit, - navigateToMedia: (ImmutableList, Int) -> Unit, + navigateToMedia: (ImmutableList, Int) -> Unit, ) { val context = LocalContext.current var refreshing by remember { mutableStateOf(false) } @@ -151,3 +159,107 @@ fun StatusCommonList( } } } + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun StatusCommonList( + statusCommonListData: StatusCommonListData, + statusListState: LazyListState, + enablePullRefresh: Boolean = false, + action: (StatusAction, Status) -> Unit, + refreshList: () -> Unit, + append: () -> Unit, + navigateToDetail: (Status) -> Unit, + navigateToProfile: (Account) -> Unit, + navigateToMedia: (ImmutableList, Int) -> Unit, +) { + val statusList by remember(statusCommonListData.timeline) { + mutableStateOf(statusCommonListData.timeline) + } + val loadState by remember(statusCommonListData.loadState) { + mutableStateOf(statusCommonListData.loadState) + } + + val context = LocalContext.current + var refreshing by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = { + scope.launch { + refreshing = true + delay(500) + refreshList() + refreshing = false + } + } + ) + Box( + modifier = Modifier + .fillMaxSize() + .let { + if (enablePullRefresh) it.pullRefresh(pullRefreshState) else it + } + ) { + when (statusList.size) { + 0 -> { + when { + loadState == Error -> StatusListLoadError { refreshList() } + loadState == NotLoading && statusCommonListData.endReached -> + EmptyStatusListPlaceholder( + pageType = PageType.Timeline, + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) + ) + else -> StatusListLoading(Modifier.fillMaxSize()) + } + } + else -> { + LazyColumn( + state = statusListState, + modifier = Modifier.fillMaxSize().padding(bottom = 100.dp), + ) { + itemsIndexed( + items = statusCommonListData.timeline, + contentType = { _, _ -> StatusUiData }, + key = { _, item -> item.id } + ) { index, status -> + val replyChainType by remember(status, statusList.size, index) { + mutableStateOf(statusList.getReplyChainType(index)) + } + val hasUnloadedParent by remember(status, statusList.size, index) { + mutableStateOf(statusList.hasUnloadedParent(index)) + } + StatusListItem( + status = status, + action = { + action(it, status.actionable) + }, + replyChainType = replyChainType, + hasUnloadedParent = hasUnloadedParent, + navigateToDetail = { + navigateToDetail(status.actionable) + }, + navigateToProfile = navigateToProfile, + navigateToMedia = navigateToMedia, + ) + if (!status.hasUnloadedStatus && (replyChainType == End || replyChainType == Null)) + AppHorizontalDivider() + } + item { + when (loadState) { + Append -> StatusAppendingIndicator() + Error -> { + // TODO Localization + Toast.makeText(context, "获取嘟文失败,请稍后重试", Toast.LENGTH_SHORT).show() + append() // retry + } + else -> Unit + } + if (statusCommonListData.endReached) StatusEndIndicator(Modifier.padding(36.dp)) + } + } + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } + } + } +} diff --git a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusListItem.kt b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusListItem.kt index 130eff61..2c55b1f1 100644 --- a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusListItem.kt +++ b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusListItem.kt @@ -38,7 +38,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -400,9 +399,6 @@ private fun StatusActionsRow( action: (StatusAction) -> Unit, modifier: Modifier = Modifier ) { - var animatedFavCount by remember(favoritesCount) { mutableIntStateOf(favoritesCount) } - var animatedReblogCount by remember(reblogsCount) { mutableIntStateOf(reblogsCount) } - CenterRow(modifier = modifier) { CenterRow( modifier = Modifier.weight(1f), @@ -427,12 +423,11 @@ private fun StatusActionsRow( favorited = favorited, modifier = Modifier.size(statusActionsIconSize) ) { - if (it) animatedFavCount++ else animatedFavCount-- action(StatusAction.Favorite(statusId, it)) } - if (animatedFavCount != 0) WidthSpacer(value = 2.dp) + if (favoritesCount != 0) WidthSpacer(value = 2.dp) AnimatedText( - text = if (animatedFavCount != 0) animatedFavCount.toString() else "", + text = if (favoritesCount != 0) favoritesCount.toString() else "", style = AppTheme.typography.statusActions, ) } @@ -442,12 +437,11 @@ private fun StatusActionsRow( enabled = rebloggingAllowed, modifier = Modifier.size(statusActionsIconSize) ) { - if (it) animatedReblogCount++ else animatedReblogCount-- action(StatusAction.Reblog(statusId, it)) } WidthSpacer(value = 2.dp) AnimatedText( - text = if (animatedReblogCount != 0) animatedReblogCount.toString() else "", + text = if (reblogsCount != 0) reblogsCount.toString() else "", style = TextStyle(color = AppTheme.colors.cardAction), ) } diff --git a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/action/FavoriteButton.kt b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/action/FavoriteButton.kt index 0ef2c98a..1f1da8fb 100644 --- a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/action/FavoriteButton.kt +++ b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/action/FavoriteButton.kt @@ -20,7 +20,6 @@ package com.github.whitescent.mastify.ui.component.status.action import androidx.compose.animation.animateColorAsState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -35,17 +34,14 @@ fun FavoriteButton( unfavoritedColor: Color = AppTheme.colors.cardAction, onClick: (Boolean) -> Unit, ) { - // var favState by remember(favorited) { mutableStateOf(favorited) } val animatedFavIconColor by animateColorAsState( targetValue = if (favorited) AppTheme.colors.cardLike else unfavoritedColor, ) - ClickableIcon( painter = painterResource(id = if (favorited) R.drawable.heart_fill else R.drawable.heart), modifier = modifier, tint = animatedFavIconColor, ) { - // favState = !favState onClick(!favorited) } } diff --git a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/action/ReblogButton.kt b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/action/ReblogButton.kt index c271837b..32f6197f 100644 --- a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/action/ReblogButton.kt +++ b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/action/ReblogButton.kt @@ -22,10 +22,8 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable 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.draw.rotate import androidx.compose.ui.draw.scale @@ -46,29 +44,27 @@ fun ReblogButton( ) { val scope = rememberCoroutineScope() - val reblogScaleAnimatable = remember { Animatable(1f) } - val reblogRotateAnimatable = remember { Animatable(0f) } - var reblogState by remember(reblogged) { mutableStateOf(reblogged) } + val scaleAnimatable = remember { Animatable(1f) } + val rotateAnimatable = remember { Animatable(0f) } val animatedReblogIconColor by animateColorAsState( - targetValue = if (reblogState) AppTheme.colors.reblogged else unreblogColor, + targetValue = if (reblogged) AppTheme.colors.reblogged else unreblogColor, ) ClickableIcon( - painter = painterResource(if (reblogState) R.drawable.share_fill else R.drawable.share_fat), - modifier = modifier.scale(reblogScaleAnimatable.value).rotate(reblogRotateAnimatable.value), + painter = painterResource(if (reblogged) R.drawable.share_fill else R.drawable.share_fat), + modifier = modifier.scale(scaleAnimatable.value).rotate(rotateAnimatable.value), tint = if (enabled) animatedReblogIconColor else unreblogColor.copy(0.34f), enabled = enabled ) { if (enabled) { - reblogState = !reblogState - onClick(reblogState) + onClick(!reblogged) scope.launch { - reblogRotateAnimatable.animateTo( - targetValue = if (reblogRotateAnimatable.value == 0f) 360f else 0f, + rotateAnimatable.animateTo( + targetValue = if (rotateAnimatable.value == 0f) 360f else 0f, animationSpec = tween(durationMillis = 300) ) - reblogScaleAnimatable.animateTo(1.4f, animationSpec = tween(durationMillis = 150)) - reblogScaleAnimatable.animateTo(1f, animationSpec = tween(durationMillis = 150)) + scaleAnimatable.animateTo(1.4f, animationSpec = tween(durationMillis = 150)) + scaleAnimatable.animateTo(1f, animationSpec = tween(durationMillis = 150)) } } } diff --git a/app/src/main/java/com/github/whitescent/mastify/viewModel/ExplorerViewModel.kt b/app/src/main/java/com/github/whitescent/mastify/viewModel/ExplorerViewModel.kt index e02f5484..ae33f0a0 100644 --- a/app/src/main/java/com/github/whitescent/mastify/viewModel/ExplorerViewModel.kt +++ b/app/src/main/java/com/github/whitescent/mastify/viewModel/ExplorerViewModel.kt @@ -19,37 +19,45 @@ package com.github.whitescent.mastify.viewModel import android.content.Context import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import androidx.paging.cachedIn import com.github.whitescent.R import com.github.whitescent.mastify.data.model.ui.StatusUiData import com.github.whitescent.mastify.data.repository.ExploreRepository import com.github.whitescent.mastify.data.repository.SearchPreviewResult import com.github.whitescent.mastify.database.AppDatabase import com.github.whitescent.mastify.domain.StatusActionHandler +import com.github.whitescent.mastify.mapper.status.toUiData +import com.github.whitescent.mastify.network.MastodonApi import com.github.whitescent.mastify.network.model.search.SearchResult +import com.github.whitescent.mastify.network.model.status.Status +import com.github.whitescent.mastify.paging.LoadState +import com.github.whitescent.mastify.paging.Paginator import com.github.whitescent.mastify.utils.StatusAction +import com.github.whitescent.mastify.utils.StatusAction.Bookmark +import com.github.whitescent.mastify.utils.StatusAction.Favorite +import com.github.whitescent.mastify.utils.StatusAction.Reblog import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -57,12 +65,129 @@ import javax.inject.Inject @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) class ExplorerViewModel @Inject constructor( db: AppDatabase, + private val api: MastodonApi, private val statusActionHandler: StatusActionHandler, private val exploreRepository: ExploreRepository, ) : ViewModel() { private val accountDao = db.accountDao() - private val activityAccountFlow = accountDao.getActiveAccountFlow() + private val activityAccountFlow = accountDao + .getActiveAccountFlow() + .distinctUntilChanged { old, new -> old?.id == new?.id } + .filterNotNull() + + private var trendingFlow = MutableStateFlow(StatusCommonListData()) + private var publicTimelineFlow = MutableStateFlow(StatusCommonListData()) + + private var currentExploreKindFlow = MutableStateFlow(ExplorerKind.Trending) + val currentExploreKind = currentExploreKindFlow + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = ExplorerKind.Trending + ) + + val trending = trendingFlow + .map { + StatusCommonListData( + timeline = it.timeline.map { status -> status.toUiData() }, + loadState = it.loadState, + endReached = it.endReached + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = StatusCommonListData() + ) + + val publicTimeline = publicTimelineFlow + .map { + StatusCommonListData( + timeline = it.timeline.map { status -> status.toUiData() }, + loadState = it.loadState, + endReached = it.endReached + ) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = StatusCommonListData() + ) + + val trendingPaginator = Paginator( + refreshKey = 0, + getAppendKey = { + trendingFlow.value.timeline.size + }, + onLoadUpdated = { + trendingFlow.value = trendingFlow.value.copy(loadState = it) + }, + onError = { it?.printStackTrace() }, + onRequest = { nextPage -> + val response = api.trendingStatus(offset = nextPage) + if (response.isSuccessful && !response.body().isNullOrEmpty()) { + val body = response.body()!! + Result.success(body) + } else Result.success(emptyList()) + }, + onSuccess = { state, item -> + when (state) { + LoadState.Append -> { + val timeline = trendingFlow.value.timeline + trendingFlow.emit( + trendingFlow.value.copy( + timeline = timeline + item, + endReached = item.isEmpty() + ) + ) + } + LoadState.Refresh -> { + trendingFlow.emit( + trendingFlow.value.copy(timeline = item, endReached = item.isEmpty()) + ) + } + else -> Unit + } + } + ) + + val publicTimelinePaginator = Paginator( + refreshKey = null, + getAppendKey = { + publicTimelineFlow.value.timeline.lastOrNull()?.id + }, + onLoadUpdated = { + publicTimelineFlow.value = publicTimelineFlow.value.copy(loadState = it) + }, + onError = { it?.printStackTrace() }, + onRequest = { nextPage -> + val response = api.publicTimeline(maxId = nextPage, local = true) + if (response.isSuccessful && !response.body().isNullOrEmpty()) { + val body = response.body()!! + Result.success(body) + } else Result.success(emptyList()) + }, + onSuccess = { state, item -> + when (state) { + LoadState.Append -> { + val timeline = publicTimelineFlow.value.timeline + publicTimelineFlow.emit( + publicTimelineFlow.value.copy( + timeline = timeline + item, + endReached = item.isEmpty() + ) + ) + } + LoadState.Refresh -> { + publicTimelineFlow.emit( + publicTimelineFlow.value.copy(timeline = item, endReached = item.isEmpty()) + ) + } + else -> Unit + } + } + ) private val searchErrorChannel = Channel() val searchErrorFlow = searchErrorChannel.receiveAsFlow() @@ -91,30 +216,38 @@ class ExplorerViewModel @Inject constructor( initialValue = null ) - val trendingStatusPager: Flow> = activityAccountFlow - .filterNotNull() - .map { it.id } - .distinctUntilChanged() - .flatMapLatest { exploreRepository.getTrendingStatusPager() } - .cachedIn(viewModelScope) - - val publicTimelinePager: Flow> = activityAccountFlow - .filterNotNull() - .map { it.id } - .distinctUntilChanged() - .flatMapLatest { exploreRepository.getPublicTimelinePager() } - .cachedIn(viewModelScope) - init { viewModelScope.launch { - activityAccountFlow.collect { - it?.let { + launch { + activityAccountFlow.collect { uiState = uiState.copy( avatar = it.profilePictureUrl, userInstance = it.domain ) } } + trendingPaginator.refresh() + publicTimelinePaginator.refresh() + } + } + + fun refreshExploreKind(kind: ExplorerKind) { + viewModelScope.launch { + when (kind) { + ExplorerKind.Trending -> trendingPaginator.refresh() + ExplorerKind.PublicTimeline -> publicTimelinePaginator.refresh() + ExplorerKind.News -> TODO() + } + } + } + + fun appendExploreKind(kind: ExplorerKind) { + viewModelScope.launch { + when (kind) { + ExplorerKind.Trending -> trendingPaginator.append() + ExplorerKind.PublicTimeline -> publicTimelinePaginator.append() + ExplorerKind.News -> TODO() + } } } @@ -126,11 +259,64 @@ class ExplorerViewModel @Inject constructor( uiState = uiState.copy(text = "") } - fun onStatusAction(action: StatusAction, context: Context) = viewModelScope.launch { - statusActionHandler.onStatusAction(action, context) + fun onStatusAction(action: StatusAction, context: Context, kind: ExplorerKind, status: Status) { + viewModelScope.launch(Dispatchers.IO) { + val favorite = (action as? Favorite)?.favorite ?: status.favorited + val favouritesCount = (action as? Favorite)?.let { state -> + status.favouritesCount + if (state.favorite) 1 else -1 + } ?: status.favouritesCount + + val reblog = (action as? Reblog)?.reblog ?: status.reblogged + val reblogsCount = (action as? Reblog)?.let { state -> + status.reblogsCount + if (state.reblog) 1 else -1 + } ?: status.reblogsCount + + val bookmark = (action as? Bookmark)?.bookmark ?: status.bookmarked + + val timeline = when (kind) { + ExplorerKind.Trending -> trendingFlow.value.timeline + ExplorerKind.PublicTimeline -> publicTimelineFlow.value.timeline + else -> emptyList() + } + if (timeline.isNotEmpty()) { + val index = timeline.indexOfFirst { it.id == status.id } + if (index != -1) { + val newTimeline = timeline.toMutableList() + newTimeline[index] = newTimeline[index].copy( + favorited = favorite, + favouritesCount = favouritesCount, + reblogged = reblog, + reblogsCount = reblogsCount, + bookmarked = bookmark, + ) + when (kind) { + ExplorerKind.Trending -> trendingFlow.update { it.copy(timeline = newTimeline) } + ExplorerKind.PublicTimeline -> + publicTimelineFlow.update { it.copy(timeline = newTimeline) } + else -> Unit + } + } + } + statusActionHandler.onStatusAction(action, context) + } + } + + fun syncExploreKind(page: Int) { + currentExploreKindFlow.value = ExplorerKind.entries.toTypedArray()[page] + } + + companion object { + const val EXPLOREPAGINGFETCHNUMBER = 20 } } +@Immutable +data class StatusCommonListData ( + val timeline: List = emptyList(), + val loadState: LoadState = LoadState.NotLoading, + val endReached: Boolean = false +) + data class ExploreUiState( val avatar: String = "", val text: String = "", diff --git a/app/src/main/java/com/github/whitescent/mastify/viewModel/HomeViewModel.kt b/app/src/main/java/com/github/whitescent/mastify/viewModel/HomeViewModel.kt index f2865504..258a6a8b 100644 --- a/app/src/main/java/com/github/whitescent/mastify/viewModel/HomeViewModel.kt +++ b/app/src/main/java/com/github/whitescent/mastify/viewModel/HomeViewModel.kt @@ -69,7 +69,7 @@ class HomeViewModel @Inject constructor( private var timelineMemoryFlow = MutableStateFlow>(emptyList()) - private val paginator = Paginator( + val paginator = Paginator( getAppendKey = { timelineMemoryFlow.value.lastOrNull()?.id }, diff --git a/app/src/main/java/com/github/whitescent/mastify/viewModel/StatusDetailViewModel.kt b/app/src/main/java/com/github/whitescent/mastify/viewModel/StatusDetailViewModel.kt index 7328fb8d..bb4c27a6 100644 --- a/app/src/main/java/com/github/whitescent/mastify/viewModel/StatusDetailViewModel.kt +++ b/app/src/main/java/com/github/whitescent/mastify/viewModel/StatusDetailViewModel.kt @@ -195,8 +195,7 @@ class StatusDetailViewModel @Inject constructor( if (navArgs.originStatusId == null) return viewModelScope.launch { val activeAccountId = accountDao.getActiveAccount()!!.id - var savedStatus = - timelineDao.getSingleStatusWithId(activeAccountId, navArgs.originStatusId) + var savedStatus = timelineDao.getSingleStatusWithId(activeAccountId, navArgs.originStatusId) savedStatus?.let { when (it.reblog == null) { true -> { From 200fefadf18fee629c151aac00a30665c800b43a Mon Sep 17 00:00:00 2001 From: whitescent Date: Thu, 30 Nov 2023 11:10:37 +0800 Subject: [PATCH 2/5] fix: some issues --- .../whitescent/mastify/paging/Paginator.kt | 32 ----------- .../mastify/screen/explore/Explore.kt | 55 +++++++++++++------ .../whitescent/mastify/screen/home/Home.kt | 23 +++++--- .../mastify/screen/profile/ProfilePager.kt | 3 +- .../mastify/ui/component/StatusListUtils.kt | 2 +- .../ui/component/status/StatusCommonList.kt | 5 +- 6 files changed, 60 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/github/whitescent/mastify/paging/Paginator.kt b/app/src/main/java/com/github/whitescent/mastify/paging/Paginator.kt index 0508545a..f4d4cc65 100644 --- a/app/src/main/java/com/github/whitescent/mastify/paging/Paginator.kt +++ b/app/src/main/java/com/github/whitescent/mastify/paging/Paginator.kt @@ -17,15 +17,8 @@ package com.github.whitescent.mastify.paging -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import com.github.whitescent.mastify.paging.LoadState.Error import com.github.whitescent.mastify.paging.LoadState.NotLoading -import kotlinx.coroutines.launch class Paginator( private val refreshKey: Key, @@ -90,31 +83,6 @@ class Paginator( } } -@Composable -fun LaunchPaginatorListener( - lazyListState: LazyListState, - list: List, - paginator: Paginator<*, *>, - fetchNumber: Int, - threshold: Int = 10 -) { - val firstVisibleItemIndex by remember(lazyListState) { - derivedStateOf { - lazyListState.firstVisibleItemIndex - } - } - if (list.isNotEmpty()) { - if (!paginator.endReached && paginator.loadState == NotLoading && - firstVisibleItemIndex >= (list.size - ((list.size / fetchNumber) * threshold)) - ) { - val scope = rememberCoroutineScope() - scope.launch { - paginator.append() - } - } - } -} - enum class LoadState { Refresh, Append, Error, NotLoading } diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt b/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt index 4b1e7786..57b0cbcd 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt @@ -83,8 +83,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.gigamole.composeshadowsplus.rsblur.rsBlurShadow import com.github.whitescent.R import com.github.whitescent.mastify.AppNavGraph +import com.github.whitescent.mastify.data.repository.HomeRepository.Companion.PAGINGTHRESHOLD import com.github.whitescent.mastify.network.model.search.SearchResult -import com.github.whitescent.mastify.paging.LaunchPaginatorListener +import com.github.whitescent.mastify.paging.LoadState.NotLoading import com.github.whitescent.mastify.screen.destinations.ProfileDestination import com.github.whitescent.mastify.screen.destinations.StatusDetailDestination import com.github.whitescent.mastify.screen.destinations.StatusMediaScreenDestination @@ -99,10 +100,14 @@ import com.github.whitescent.mastify.ui.theme.AppTheme import com.github.whitescent.mastify.ui.transitions.BottomBarScreenTransitions import com.github.whitescent.mastify.utils.AppState import com.github.whitescent.mastify.viewModel.ExplorerKind +import com.github.whitescent.mastify.viewModel.ExplorerKind.PublicTimeline +import com.github.whitescent.mastify.viewModel.ExplorerKind.Trending import com.github.whitescent.mastify.viewModel.ExplorerViewModel import com.github.whitescent.mastify.viewModel.ExplorerViewModel.Companion.EXPLOREPAGINGFETCHNUMBER import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -265,7 +270,7 @@ fun Explore( ) } - LaunchedEffect(currentExploreKind) { + LaunchedEffect(Unit) { launch { viewModel.snackBarFlow.collect { snackbarState.show(it) @@ -273,8 +278,11 @@ fun Explore( } launch { appState.scrollToTopFlow.collect { - trendingStatusListState.scrollToItem(0) - publicTimelineListState.scrollToItem(0) + when (currentExploreKind) { + Trending -> trendingStatusListState.scrollToItem(0) + PublicTimeline -> publicTimelineListState.scrollToItem(0) + else -> Unit + } } } launch { @@ -282,19 +290,34 @@ fun Explore( Toast.makeText(context, "搜索失败", Toast.LENGTH_SHORT).show() } } + // TODO There is a need to encapsulate a layer of methods for the pagination's append request, + // but I haven't thought of a suitable way to do this yet, + // I tried wrapping it into a @Composable, but it causes LeftCompositionCancellationException + launch { + snapshotFlow { trendingStatusListState.firstVisibleItemIndex } + .filter { trendingStatusList.timeline.isNotEmpty() } + .map { + !viewModel.trendingPaginator.endReached && viewModel.trendingPaginator.loadState == NotLoading && + it >= (trendingStatusList.timeline.size - ((trendingStatusList.timeline.size / EXPLOREPAGINGFETCHNUMBER) * PAGINGTHRESHOLD)) + } + .filter { it } + .collect { + viewModel.trendingPaginator.append() + } + } + launch { + snapshotFlow { publicTimelineListState.firstVisibleItemIndex } + .filter { publicTimelineList.timeline.isNotEmpty() } + .map { + !viewModel.publicTimelinePaginator.endReached && viewModel.publicTimelinePaginator.loadState == NotLoading && + it >= (publicTimelineList.timeline.size - ((publicTimelineList.timeline.size / EXPLOREPAGINGFETCHNUMBER) * PAGINGTHRESHOLD)) + } + .filter { it } + .collect { + viewModel.publicTimelinePaginator.append() + } + } } - LaunchPaginatorListener( - lazyListState = trendingStatusListState, - list = trendingStatusList.timeline, - paginator = viewModel.trendingPaginator, - fetchNumber = EXPLOREPAGINGFETCHNUMBER - ) - LaunchPaginatorListener( - lazyListState = publicTimelineListState, - list = publicTimelineList.timeline, - paginator = viewModel.publicTimelinePaginator, - fetchNumber = EXPLOREPAGINGFETCHNUMBER - ) } @Composable diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/home/Home.kt b/app/src/main/java/com/github/whitescent/mastify/screen/home/Home.kt index 922a899c..be335862 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/home/Home.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/home/Home.kt @@ -86,7 +86,6 @@ import com.github.whitescent.mastify.data.repository.HomeRepository.Companion.PA import com.github.whitescent.mastify.database.model.AccountEntity import com.github.whitescent.mastify.mapper.status.getReplyChainType import com.github.whitescent.mastify.mapper.status.hasUnloadedParent -import com.github.whitescent.mastify.paging.LaunchPaginatorListener import com.github.whitescent.mastify.paging.LoadState import com.github.whitescent.mastify.paging.LoadState.Error import com.github.whitescent.mastify.paging.LoadState.NotLoading @@ -119,6 +118,8 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class, FlowPreview::class) @@ -300,6 +301,18 @@ fun Home( LaunchedEffect(firstVisibleIndex) { if (firstVisibleIndex == 0 && uiState.showNewStatusButton) viewModel.dismissButton() + launch { + snapshotFlow { firstVisibleIndex } + .filter { timeline.isNotEmpty() } + .map { + !uiState.endReached && uiState.timelineLoadState == NotLoading && + lazyState.firstVisibleItemIndex >= (timeline.size - ((timeline.size / FETCHNUMBER) * PAGINGTHRESHOLD)) + } + .filter { it } + .collect { + viewModel.append() + } + } launch { snapshotFlow { firstVisibleIndex } .debounce(500L) @@ -309,14 +322,6 @@ fun Home( } } } - - LaunchPaginatorListener( - lazyListState = lazyState, - list = timeline, - paginator = viewModel.paginator, - fetchNumber = FETCHNUMBER, - threshold = PAGINGTHRESHOLD - ) } @Composable diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/profile/ProfilePager.kt b/app/src/main/java/com/github/whitescent/mastify/screen/profile/ProfilePager.kt index fbe11fdc..4d6143d5 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/profile/ProfilePager.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/profile/ProfilePager.kt @@ -26,6 +26,7 @@ import androidx.paging.compose.LazyPagingItems import com.github.whitescent.mastify.data.model.ui.StatusUiData import com.github.whitescent.mastify.network.model.account.Account import com.github.whitescent.mastify.network.model.status.Status +import com.github.whitescent.mastify.network.model.status.Status.Attachment import com.github.whitescent.mastify.ui.component.status.StatusCommonList import com.github.whitescent.mastify.utils.StatusAction import kotlinx.collections.immutable.ImmutableList @@ -43,7 +44,7 @@ fun ProfilePager( action: (StatusAction) -> Unit, navigateToDetail: (Status) -> Unit, navigateToProfile: (Account) -> Unit, - navigateToMedia: (ImmutableList, Int) -> Unit, + navigateToMedia: (ImmutableList, Int) -> Unit, ) { HorizontalPager( state = state, diff --git a/app/src/main/java/com/github/whitescent/mastify/ui/component/StatusListUtils.kt b/app/src/main/java/com/github/whitescent/mastify/ui/component/StatusListUtils.kt index 4e6cc91c..f2c06f11 100644 --- a/app/src/main/java/com/github/whitescent/mastify/ui/component/StatusListUtils.kt +++ b/app/src/main/java/com/github/whitescent/mastify/ui/component/StatusListUtils.kt @@ -113,7 +113,7 @@ fun StatusEndIndicator( ) { Box( modifier = modifier.fillMaxWidth(), - contentAlignment = Alignment.Center + contentAlignment = Alignment.TopCenter ) { Box(Modifier.size(4.dp).background(Color.Gray, CircleShape)) } diff --git a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusCommonList.kt b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusCommonList.kt index 8a6e945f..b79d0f59 100644 --- a/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusCommonList.kt +++ b/app/src/main/java/com/github/whitescent/mastify/ui/component/status/StatusCommonList.kt @@ -19,6 +19,7 @@ package com.github.whitescent.mastify.ui.component.status import android.widget.Toast import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -165,6 +166,7 @@ fun StatusCommonList( fun StatusCommonList( statusCommonListData: StatusCommonListData, statusListState: LazyListState, + modifier: Modifier = Modifier, enablePullRefresh: Boolean = false, action: (StatusAction, Status) -> Unit, refreshList: () -> Unit, @@ -216,7 +218,8 @@ fun StatusCommonList( else -> { LazyColumn( state = statusListState, - modifier = Modifier.fillMaxSize().padding(bottom = 100.dp), + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 100.dp) ) { itemsIndexed( items = statusCommonListData.timeline, From cdcd10844ad6bc5a9a691f6be6aff67faf9c8a8b Mon Sep 17 00:00:00 2001 From: whitescent Date: Fri, 1 Dec 2023 00:16:01 +0800 Subject: [PATCH 3/5] feat: add crossfade for coil image --- app/src/main/java/com/github/whitescent/mastify/MastifyApp.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/github/whitescent/mastify/MastifyApp.kt b/app/src/main/java/com/github/whitescent/mastify/MastifyApp.kt index df7354b7..635d5c15 100644 --- a/app/src/main/java/com/github/whitescent/mastify/MastifyApp.kt +++ b/app/src/main/java/com/github/whitescent/mastify/MastifyApp.kt @@ -44,6 +44,7 @@ class MastifyApp : Application(), ImageLoaderFactory { add(VideoFrameDecoder.Factory()) } .placeholder(R.drawable.image_placeholder) + .crossfade(true) .build() } From e4d08782ed248c14528e7e46baa7c9154993a148 Mon Sep 17 00:00:00 2001 From: whitescent Date: Fri, 1 Dec 2023 16:55:59 +0800 Subject: [PATCH 4/5] feat: add news section --- .../data/repository/ExploreRepository.kt | 18 ++- .../whitescent/mastify/network/MastodonApi.kt | 7 ++ .../mastify/network/model/status/Hashtag.kt | 7 +- .../mastify/network/model/status/History.kt | 27 ++++ .../mastify/network/model/trends/News.kt | 33 +++++ .../mastify/screen/explore/Explore.kt | 5 + .../mastify/screen/explore/ExploreNews.kt | 118 ++++++++++++++++++ .../mastify/screen/explore/ExplorePager.kt | 31 +++++ .../mastify/screen/profile/Profile.kt | 2 +- .../mastify/utils/launchCustomTabs.kt | 6 +- .../mastify/viewModel/ExplorerViewModel.kt | 20 +-- 11 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/github/whitescent/mastify/network/model/status/History.kt create mode 100644 app/src/main/java/com/github/whitescent/mastify/network/model/trends/News.kt create mode 100644 app/src/main/java/com/github/whitescent/mastify/screen/explore/ExploreNews.kt diff --git a/app/src/main/java/com/github/whitescent/mastify/data/repository/ExploreRepository.kt b/app/src/main/java/com/github/whitescent/mastify/data/repository/ExploreRepository.kt index 6c6c3e60..af25ceaa 100644 --- a/app/src/main/java/com/github/whitescent/mastify/data/repository/ExploreRepository.kt +++ b/app/src/main/java/com/github/whitescent/mastify/data/repository/ExploreRepository.kt @@ -17,32 +17,28 @@ package com.github.whitescent.mastify.data.repository +import at.connyduck.calladapter.networkresult.NetworkResult import at.connyduck.calladapter.networkresult.fold import com.github.whitescent.mastify.network.MastodonApi import com.github.whitescent.mastify.network.model.search.SearchResult +import com.github.whitescent.mastify.network.model.trends.News import javax.inject.Inject -import javax.inject.Singleton -@Singleton class ExploreRepository @Inject constructor( private val api: MastodonApi ) { - - suspend fun getPreviewResultsForSearch(keyword: String): SearchPreviewResult { - if (keyword.isBlank()) return SearchPreviewResult.Success(null) + suspend fun getPreviewResultsForSearch(keyword: String): NetworkResult { + if (keyword.isBlank()) return NetworkResult.success(null) return api.searchSync(query = keyword, limit = 10).fold( { - SearchPreviewResult.Success(it) + NetworkResult.success(it) }, { it.printStackTrace() - SearchPreviewResult.Failure(it) + NetworkResult.failure(it) } ) } -} -sealed interface SearchPreviewResult { - data class Success(val response: SearchResult?) : SearchPreviewResult - data class Failure(val exception: Throwable) : SearchPreviewResult + suspend fun getNews(): NetworkResult> = api.trendingNews(10) } diff --git a/app/src/main/java/com/github/whitescent/mastify/network/MastodonApi.kt b/app/src/main/java/com/github/whitescent/mastify/network/MastodonApi.kt index b62f058b..d637a43c 100644 --- a/app/src/main/java/com/github/whitescent/mastify/network/MastodonApi.kt +++ b/app/src/main/java/com/github/whitescent/mastify/network/MastodonApi.kt @@ -28,6 +28,7 @@ import com.github.whitescent.mastify.network.model.search.SearchResult import com.github.whitescent.mastify.network.model.status.NewStatus import com.github.whitescent.mastify.network.model.status.Status import com.github.whitescent.mastify.network.model.status.StatusContext +import com.github.whitescent.mastify.network.model.trends.News import retrofit2.Response import retrofit2.http.Body import retrofit2.http.Field @@ -96,6 +97,12 @@ interface MastodonApi { @Query("offset") offset: Int = 0 ): Response> + @GET("api/v1/trends/links") + suspend fun trendingNews( + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int = 0 + ): NetworkResult> + @POST("api/v1/statuses/{id}/bookmark") suspend fun bookmarkStatus( @Path("id") statusId: String, diff --git a/app/src/main/java/com/github/whitescent/mastify/network/model/status/Hashtag.kt b/app/src/main/java/com/github/whitescent/mastify/network/model/status/Hashtag.kt index 6cbd535a..bd0fa94b 100644 --- a/app/src/main/java/com/github/whitescent/mastify/network/model/status/Hashtag.kt +++ b/app/src/main/java/com/github/whitescent/mastify/network/model/status/Hashtag.kt @@ -20,4 +20,9 @@ package com.github.whitescent.mastify.network.model.status import kotlinx.serialization.Serializable @Serializable -data class Hashtag(val name: String, val url: String, val following: Boolean? = null) +data class Hashtag( + val name: String, + val url: String, + val following: Boolean?, + val history: List? +) diff --git a/app/src/main/java/com/github/whitescent/mastify/network/model/status/History.kt b/app/src/main/java/com/github/whitescent/mastify/network/model/status/History.kt new file mode 100644 index 00000000..3d56b78f --- /dev/null +++ b/app/src/main/java/com/github/whitescent/mastify/network/model/status/History.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2023 WhiteScent + * + * This file is a part of Mastify. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Mastify is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Mastify; if not, + * see . + */ + +package com.github.whitescent.mastify.network.model.status + +import kotlinx.serialization.Serializable + +@Serializable +data class History( + val day: String, + val uses: String, + val accounts: String +) diff --git a/app/src/main/java/com/github/whitescent/mastify/network/model/trends/News.kt b/app/src/main/java/com/github/whitescent/mastify/network/model/trends/News.kt new file mode 100644 index 00000000..e17fac30 --- /dev/null +++ b/app/src/main/java/com/github/whitescent/mastify/network/model/trends/News.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 WhiteScent + * + * This file is a part of Mastify. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Mastify is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Mastify; if not, + * see . + */ + +package com.github.whitescent.mastify.network.model.trends + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class News( + val url: String, + val title: String, + val description: String, + @SerialName("author_name") val authorname: String, + @SerialName("provider_name") val providername: String, + @SerialName("published_at") val publishedAt: String?, + val image: String, + val blurhash: String +) diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt b/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt index 57b0cbcd..dd7a8498 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/explore/Explore.kt @@ -142,6 +142,9 @@ fun Explore( val publicTimelineListState = rememberLazyListState() val publicTimelineList by viewModel.publicTimeline.collectAsStateWithLifecycle() + val newsListState = rememberLazyListState() + val newsList = uiState.trendingNews + val searchingResult by viewModel.searchPreviewResult.collectAsStateWithLifecycle() Box( @@ -224,6 +227,8 @@ fun Explore( trendingStatusList = trendingStatusList, publicTimelineListState = publicTimelineListState, publicTimelineList = publicTimelineList, + newsListState = newsListState, + newsList = newsList, action = { action, kind, status -> viewModel.onStatusAction(action, context, kind, status) }, diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExploreNews.kt b/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExploreNews.kt new file mode 100644 index 00000000..ea0659fc --- /dev/null +++ b/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExploreNews.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2023 WhiteScent + * + * This file is a part of Mastify. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Mastify is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Mastify; if not, + * see . + */ + +package com.github.whitescent.mastify.screen.explore + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +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.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.github.whitescent.mastify.network.model.trends.News +import com.github.whitescent.mastify.ui.component.AsyncBlurImage +import com.github.whitescent.mastify.ui.component.CenterRow +import com.github.whitescent.mastify.ui.component.HeightSpacer +import com.github.whitescent.mastify.ui.component.WidthSpacer +import com.github.whitescent.mastify.ui.theme.AppTheme +import com.github.whitescent.mastify.utils.getRelativeTimeSpanString +import com.github.whitescent.mastify.utils.launchCustomChromeTab +import kotlinx.datetime.Clock +import kotlinx.datetime.toInstant + +@Composable +fun ExploreNewsItem( + news: News +) { + val context = LocalContext.current + val toolbarColor = AppTheme.colors.primaryContent + Box( + modifier = Modifier + .fillMaxWidth() + .clip(AppTheme.shape.mediumAvatar) + .height(180.dp) + .clickable { + launchCustomChromeTab( + context = context, + uri = Uri.parse(news.url), + toolbarColor = toolbarColor.toArgb(), + ) + } + ) { + AsyncBlurImage( + url = news.image, + blurHash = news.blurhash, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.4f))) + Column( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Text( + text = news.title, + color = Color.White, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + overflow = Ellipsis + ) + HeightSpacer(value = 4.dp) + CenterRow { + Text( + text = news.providername, + color = Color.White, + fontSize = 14.sp + ) + news.publishedAt?.let { + WidthSpacer(value = 4.dp) + Box(Modifier.size(2.dp).background(Color.White, CircleShape)) + WidthSpacer(value = 4.dp) + Text( + text = getRelativeTimeSpanString( + context, + news.publishedAt.toInstant().toEpochMilliseconds(), + Clock.System.now().toEpochMilliseconds() + ), + color = Color.White, + fontSize = 14.sp + ) + } + } + } + } +} diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExplorePager.kt b/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExplorePager.kt index 3c6ea09c..7fd219db 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExplorePager.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/explore/ExplorePager.kt @@ -18,16 +18,23 @@ package com.github.whitescent.mastify.screen.explore import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.github.whitescent.mastify.data.model.ui.StatusUiData import com.github.whitescent.mastify.network.model.account.Account import com.github.whitescent.mastify.network.model.status.Status +import com.github.whitescent.mastify.network.model.trends.News +import com.github.whitescent.mastify.ui.component.HeightSpacer import com.github.whitescent.mastify.ui.component.status.StatusCommonList +import com.github.whitescent.mastify.ui.component.status.paging.StatusListLoading import com.github.whitescent.mastify.utils.StatusAction import com.github.whitescent.mastify.viewModel.ExplorerKind import com.github.whitescent.mastify.viewModel.ExplorerKind.PublicTimeline @@ -43,6 +50,8 @@ fun ExplorePager( trendingStatusList: StatusCommonListData, publicTimelineListState: LazyListState, publicTimelineList: StatusCommonListData, + newsListState: LazyListState, + newsList: List?, modifier: Modifier = Modifier, action: (StatusAction, ExplorerKind, Status) -> Unit, refreshKind: (ExplorerKind) -> Unit, @@ -77,6 +86,28 @@ fun ExplorePager( navigateToProfile = navigateToProfile, navigateToMedia = navigateToMedia, ) + 2 -> { + when (newsList) { + null -> StatusListLoading(Modifier.fillMaxSize()) + else -> { + LazyColumn( + state = newsListState, + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 16.dp, + end = 16.dp, + top = 12.dp, + bottom = 150.dp + ) + ) { + items(newsList) { news -> + ExploreNewsItem(news) + HeightSpacer(value = 8.dp) + } + } + } + } + } } }, modifier = modifier.fillMaxSize(), diff --git a/app/src/main/java/com/github/whitescent/mastify/screen/profile/Profile.kt b/app/src/main/java/com/github/whitescent/mastify/screen/profile/Profile.kt index 49b395b1..f5d8ad35 100644 --- a/app/src/main/java/com/github/whitescent/mastify/screen/profile/Profile.kt +++ b/app/src/main/java/com/github/whitescent/mastify/screen/profile/Profile.kt @@ -346,7 +346,7 @@ fun ProfileTopBar( .alpha(alpha()) .drawWithContent { drawRect(defaultBackgroundColor) - this.drawContent() + drawContent() drawRect(Color.Black.copy(0.35f)) }, contentScale = ContentScale.Crop diff --git a/app/src/main/java/com/github/whitescent/mastify/utils/launchCustomTabs.kt b/app/src/main/java/com/github/whitescent/mastify/utils/launchCustomTabs.kt index 8c882349..61f02c83 100644 --- a/app/src/main/java/com/github/whitescent/mastify/utils/launchCustomTabs.kt +++ b/app/src/main/java/com/github/whitescent/mastify/utils/launchCustomTabs.kt @@ -23,7 +23,11 @@ import androidx.annotation.ColorInt import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent -fun launchCustomChromeTab(context: Context, uri: Uri, @ColorInt toolbarColor: Int) { +fun launchCustomChromeTab( + context: Context, + uri: Uri, + @ColorInt toolbarColor: Int +) { val customTabBarColor = CustomTabColorSchemeParams.Builder() .setToolbarColor(toolbarColor).build() val customTabsIntent = CustomTabsIntent.Builder() diff --git a/app/src/main/java/com/github/whitescent/mastify/viewModel/ExplorerViewModel.kt b/app/src/main/java/com/github/whitescent/mastify/viewModel/ExplorerViewModel.kt index ae33f0a0..3a60a129 100644 --- a/app/src/main/java/com/github/whitescent/mastify/viewModel/ExplorerViewModel.kt +++ b/app/src/main/java/com/github/whitescent/mastify/viewModel/ExplorerViewModel.kt @@ -26,16 +26,18 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.getOrDefault import com.github.whitescent.R import com.github.whitescent.mastify.data.model.ui.StatusUiData import com.github.whitescent.mastify.data.repository.ExploreRepository -import com.github.whitescent.mastify.data.repository.SearchPreviewResult import com.github.whitescent.mastify.database.AppDatabase import com.github.whitescent.mastify.domain.StatusActionHandler import com.github.whitescent.mastify.mapper.status.toUiData import com.github.whitescent.mastify.network.MastodonApi import com.github.whitescent.mastify.network.model.search.SearchResult +import com.github.whitescent.mastify.network.model.status.Hashtag import com.github.whitescent.mastify.network.model.status.Status +import com.github.whitescent.mastify.network.model.trends.News import com.github.whitescent.mastify.paging.LoadState import com.github.whitescent.mastify.paging.Paginator import com.github.whitescent.mastify.utils.StatusAction @@ -202,12 +204,9 @@ class ExplorerViewModel @Inject constructor( .debounce(200) .mapLatest { // reset search response when query is empty - when (val api = exploreRepository.getPreviewResultsForSearch(it)) { - is SearchPreviewResult.Success -> api.response - is SearchPreviewResult.Failure -> { - searchErrorChannel.send(Unit) - null - } + val api = exploreRepository.getPreviewResultsForSearch(it) + api.getOrNull().also { + if (api.isFailure) searchErrorChannel.send(Unit) } } .stateIn( @@ -228,6 +227,9 @@ class ExplorerViewModel @Inject constructor( } trendingPaginator.refresh() publicTimelinePaginator.refresh() + uiState = uiState.copy( + trendingNews = exploreRepository.getNews().getOrDefault(emptyList()) + ) } } @@ -320,7 +322,9 @@ data class StatusCommonListData ( data class ExploreUiState( val avatar: String = "", val text: String = "", - val userInstance: String = "" + val userInstance: String = "", + val trendingNews: List? = null, + val topics: List = emptyList() ) enum class ExplorerKind( From cd1fbd597471fbb299ddb12290559e2f456ffbf1 Mon Sep 17 00:00:00 2001 From: whitescent Date: Fri, 1 Dec 2023 16:56:20 +0800 Subject: [PATCH 5/5] build: upgrade app version --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0110f07f..e805aeae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,8 +34,8 @@ android { applicationId = "com.github.whitescent.mastify" minSdk = 21 targetSdk = 34 - versionCode = 8 - versionName = "1.1.4-pre-alpha" + versionCode = 9 + versionName = "1.2.0-pre-alpha" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true