feat: 历史记录新 UI
This commit is contained in:
parent
53df4568f3
commit
0c75052380
|
|
@ -83,6 +83,7 @@ import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
import com.huanchengfly.tieba.post.dataStore
|
import com.huanchengfly.tieba.post.dataStore
|
||||||
import com.huanchengfly.tieba.post.getInt
|
import com.huanchengfly.tieba.post.getInt
|
||||||
import com.huanchengfly.tieba.post.goToActivity
|
import com.huanchengfly.tieba.post.goToActivity
|
||||||
|
import com.huanchengfly.tieba.post.models.database.History
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
|
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
|
||||||
import com.huanchengfly.tieba.post.ui.page.forum.threadlist.ForumThreadListPage
|
import com.huanchengfly.tieba.post.ui.page.forum.threadlist.ForumThreadListPage
|
||||||
|
|
@ -102,6 +103,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.picker.ListSinglePicker
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
|
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
|
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
|
||||||
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
|
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
|
||||||
|
import com.huanchengfly.tieba.post.utils.HistoryUtil
|
||||||
import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString
|
import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString
|
||||||
import com.huanchengfly.tieba.post.utils.TiebaUtil
|
import com.huanchengfly.tieba.post.utils.TiebaUtil
|
||||||
import com.huanchengfly.tieba.post.utils.appPreferences
|
import com.huanchengfly.tieba.post.utils.appPreferences
|
||||||
|
|
@ -365,6 +367,20 @@ fun ForumPage(
|
||||||
|
|
||||||
val unlikeDialogState = rememberDialogState()
|
val unlikeDialogState = rememberDialogState()
|
||||||
|
|
||||||
|
if (forum != null) {
|
||||||
|
LaunchedEffect(forum) {
|
||||||
|
HistoryUtil.writeHistory(
|
||||||
|
History()
|
||||||
|
.setTitle(context.getString(R.string.title_forum, forumName))
|
||||||
|
.setTimestamp(System.currentTimeMillis())
|
||||||
|
.setAvatar(forum.avatar)
|
||||||
|
.setType(HistoryUtil.TYPE_FORUM)
|
||||||
|
.setData(forumName),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (account != null && forum != null) {
|
if (account != null && forum != null) {
|
||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
dialogState = unlikeDialogState,
|
dialogState = unlikeDialogState,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
package com.huanchengfly.tieba.post.ui.page.history
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.Tab
|
||||||
|
import androidx.compose.material.TabRow
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.HorizontalPager
|
||||||
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
|
import com.huanchengfly.tieba.post.R
|
||||||
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
|
import com.huanchengfly.tieba.post.ui.page.history.list.HistoryListPage
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar
|
||||||
|
import com.huanchengfly.tieba.post.utils.HistoryUtil
|
||||||
|
import com.ramcosta.composedestinations.annotation.DeepLink
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Destination(
|
||||||
|
deepLinks = [
|
||||||
|
DeepLink(uriPattern = "tblite://history")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun HistoryPage(
|
||||||
|
navigator: DestinationsNavigator
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
MyScaffold(
|
||||||
|
topBar = {
|
||||||
|
TitleCentredToolbar(
|
||||||
|
title = stringResource(id = R.string.title_history),
|
||||||
|
navigationIcon = {
|
||||||
|
BackNavigationIcon(onBackPressed = { navigator.navigateUp() })
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { /*TODO*/ }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = stringResource(id = R.string.title_history_delete),
|
||||||
|
tint = ExtendedTheme.colors.onTopBar
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
TabRow(
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
indicator = { tabPositions ->
|
||||||
|
PagerTabIndicator(
|
||||||
|
pagerState = pagerState,
|
||||||
|
tabPositions = tabPositions
|
||||||
|
)
|
||||||
|
},
|
||||||
|
divider = {},
|
||||||
|
backgroundColor = ExtendedTheme.colors.topBar,
|
||||||
|
contentColor = ExtendedTheme.colors.accent,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(100.dp * 2)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
Tab(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.title_history_thread),
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selected = pagerState.currentPage == 0,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedContentColor = ExtendedTheme.colors.accent,
|
||||||
|
unselectedContentColor = ExtendedTheme.colors.onTopBarSecondary
|
||||||
|
)
|
||||||
|
Tab(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.title_history_forum),
|
||||||
|
fontSize = 13.sp
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selected = pagerState.currentPage == 1,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedContentColor = ExtendedTheme.colors.accent,
|
||||||
|
unselectedContentColor = ExtendedTheme.colors.onTopBarSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
HorizontalPager(
|
||||||
|
count = 2,
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
userScrollEnabled = true,
|
||||||
|
) {
|
||||||
|
if (it == 0) {
|
||||||
|
HistoryListPage(type = HistoryUtil.TYPE_THREAD)
|
||||||
|
} else {
|
||||||
|
HistoryListPage(type = HistoryUtil.TYPE_FORUM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
package com.huanchengfly.tieba.post.ui.page.history.list
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.huanchengfly.tieba.post.R
|
||||||
|
import com.huanchengfly.tieba.post.activities.ForumActivity
|
||||||
|
import com.huanchengfly.tieba.post.activities.ThreadActivity
|
||||||
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
|
import com.huanchengfly.tieba.post.arch.onEvent
|
||||||
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
|
import com.huanchengfly.tieba.post.fromJson
|
||||||
|
import com.huanchengfly.tieba.post.models.ThreadHistoryInfoBean
|
||||||
|
import com.huanchengfly.tieba.post.models.database.History
|
||||||
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.Chip
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalSnackbarHostState
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LongClickMenu
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
|
||||||
|
import com.huanchengfly.tieba.post.utils.DateTimeUtils
|
||||||
|
import com.huanchengfly.tieba.post.utils.HistoryUtil
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HistoryListPage(
|
||||||
|
type: Int,
|
||||||
|
viewModel: HistoryListViewModel = if (type == HistoryUtil.TYPE_THREAD) pageViewModel<ThreadHistoryListViewModel>() else pageViewModel<ForumHistoryListViewModel>()
|
||||||
|
) {
|
||||||
|
LazyLoad(loaded = viewModel.initialized) {
|
||||||
|
viewModel.send(HistoryListUiIntent.Refresh)
|
||||||
|
viewModel.initialized = true
|
||||||
|
}
|
||||||
|
val isLoadingMore by viewModel.uiState.collectPartialAsState(
|
||||||
|
prop1 = HistoryListUiState::isLoadingMore,
|
||||||
|
initial = false
|
||||||
|
)
|
||||||
|
val hasMore by viewModel.uiState.collectPartialAsState(
|
||||||
|
prop1 = HistoryListUiState::hasMore,
|
||||||
|
initial = true
|
||||||
|
)
|
||||||
|
val currentPage by viewModel.uiState.collectPartialAsState(
|
||||||
|
prop1 = HistoryListUiState::currentPage,
|
||||||
|
initial = 0
|
||||||
|
)
|
||||||
|
val todayHistoryData by viewModel.uiState.collectPartialAsState(
|
||||||
|
prop1 = HistoryListUiState::todayHistoryData,
|
||||||
|
initial = emptyList()
|
||||||
|
)
|
||||||
|
val beforeHistoryData by viewModel.uiState.collectPartialAsState(
|
||||||
|
prop1 = HistoryListUiState::beforeHistoryData,
|
||||||
|
initial = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackbarHostState = LocalSnackbarHostState.current
|
||||||
|
LaunchedEffect(null) {
|
||||||
|
onEvent<HistoryListUiEvent.Delete.Failure>(viewModel) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.delete_history_failure,
|
||||||
|
it.errorMsg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onEvent<HistoryListUiEvent.Delete.Success>(viewModel) {
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.delete_history_success))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
LoadMoreLayout(
|
||||||
|
isLoading = isLoadingMore,
|
||||||
|
onLoadMore = { viewModel.send(HistoryListUiIntent.LoadMore(currentPage + 1)) },
|
||||||
|
loadEnd = !hasMore
|
||||||
|
) {
|
||||||
|
LazyColumn {
|
||||||
|
if (todayHistoryData.isNotEmpty()) {
|
||||||
|
stickyHeader(key = "TodayHistoryHeader") {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(ExtendedTheme.colors.background)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Chip(
|
||||||
|
text = stringResource(id = R.string.title_history_today),
|
||||||
|
invertColor = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(
|
||||||
|
items = todayHistoryData,
|
||||||
|
key = { it.id }
|
||||||
|
) { info ->
|
||||||
|
HistoryItem(
|
||||||
|
info,
|
||||||
|
onDelete = {
|
||||||
|
viewModel.send(HistoryListUiIntent.Delete(it.id))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
when (it.type) {
|
||||||
|
HistoryUtil.TYPE_FORUM -> ForumActivity.launch(
|
||||||
|
context,
|
||||||
|
it.data
|
||||||
|
)
|
||||||
|
|
||||||
|
HistoryUtil.TYPE_THREAD -> {
|
||||||
|
val extra =
|
||||||
|
if (it.extras != null) it.extras.fromJson<ThreadHistoryInfoBean>() else null
|
||||||
|
ThreadActivity.launch(
|
||||||
|
context,
|
||||||
|
it.data,
|
||||||
|
extra?.pid,
|
||||||
|
extra?.isSeeLz,
|
||||||
|
ThreadActivity.FROM_HISTORY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (beforeHistoryData.isNotEmpty()) {
|
||||||
|
stickyHeader(key = "BeforeHistoryHeader") {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(ExtendedTheme.colors.background)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Chip(text = stringResource(id = R.string.title_history_before))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(
|
||||||
|
items = beforeHistoryData,
|
||||||
|
key = { it.id }
|
||||||
|
) { info ->
|
||||||
|
HistoryItem(
|
||||||
|
info,
|
||||||
|
onDelete = {
|
||||||
|
viewModel.send(HistoryListUiIntent.Delete(it.id))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
when (it.type) {
|
||||||
|
HistoryUtil.TYPE_FORUM -> ForumActivity.launch(
|
||||||
|
context,
|
||||||
|
it.data
|
||||||
|
)
|
||||||
|
|
||||||
|
HistoryUtil.TYPE_THREAD -> {
|
||||||
|
val extra =
|
||||||
|
if (it.extras != null) it.extras.fromJson<ThreadHistoryInfoBean>() else null
|
||||||
|
ThreadActivity.launch(
|
||||||
|
context,
|
||||||
|
it.data,
|
||||||
|
extra?.pid,
|
||||||
|
extra?.isSeeLz,
|
||||||
|
ThreadActivity.FROM_HISTORY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HistoryItem(
|
||||||
|
info: History,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (History) -> Unit = {},
|
||||||
|
onDelete: (History) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val menuState = rememberMenuState()
|
||||||
|
LongClickMenu(
|
||||||
|
menuState = menuState,
|
||||||
|
menuContent = {
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
onDelete(info)
|
||||||
|
menuState.expanded = false
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(id = R.string.title_delete))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = { onClick(info) }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
UserHeader(
|
||||||
|
avatar = {
|
||||||
|
Avatar(
|
||||||
|
data = info.avatar,
|
||||||
|
size = Sizes.Small,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
name = { Text(text = if (info.type == HistoryUtil.TYPE_THREAD) info.username else info.title) },
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = DateTimeUtils.getRelativeTimeString(
|
||||||
|
LocalContext.current,
|
||||||
|
info.timestamp
|
||||||
|
),
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = ExtendedTheme.colors.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (info.type == HistoryUtil.TYPE_THREAD) {
|
||||||
|
Text(
|
||||||
|
text = info.title,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = ExtendedTheme.colors.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package com.huanchengfly.tieba.post.ui.page.history.list
|
||||||
|
|
||||||
|
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
|
||||||
|
import com.huanchengfly.tieba.post.arch.BaseViewModel
|
||||||
|
import com.huanchengfly.tieba.post.arch.PartialChange
|
||||||
|
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.database.History
|
||||||
|
import com.huanchengfly.tieba.post.utils.DateTimeUtils
|
||||||
|
import com.huanchengfly.tieba.post.utils.HistoryUtil
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import org.litepal.LitePal
|
||||||
|
import org.litepal.extension.deleteAll
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
abstract class HistoryListViewModel :
|
||||||
|
BaseViewModel<HistoryListUiIntent, HistoryListPartialChange, HistoryListUiState, HistoryListUiEvent>() {
|
||||||
|
override fun createInitialState(): HistoryListUiState = HistoryListUiState()
|
||||||
|
|
||||||
|
override fun dispatchEvent(partialChange: HistoryListPartialChange): UiEvent? {
|
||||||
|
return when (partialChange) {
|
||||||
|
is HistoryListPartialChange.Delete.Success -> HistoryListUiEvent.Delete.Success
|
||||||
|
is HistoryListPartialChange.Delete.Failure -> HistoryListUiEvent.Delete.Failure(
|
||||||
|
partialChange.error.getErrorMessage()
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ThreadHistoryListViewModel @Inject constructor() : HistoryListViewModel() {
|
||||||
|
override fun createPartialChangeProducer(): PartialChangeProducer<HistoryListUiIntent, HistoryListPartialChange, HistoryListUiState> =
|
||||||
|
HistoryListPartialChangeProducer(HistoryUtil.TYPE_THREAD)
|
||||||
|
}
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ForumHistoryListViewModel @Inject constructor() : HistoryListViewModel() {
|
||||||
|
override fun createPartialChangeProducer(): PartialChangeProducer<HistoryListUiIntent, HistoryListPartialChange, HistoryListUiState> =
|
||||||
|
HistoryListPartialChangeProducer(HistoryUtil.TYPE_FORUM)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HistoryListPartialChangeProducer(val type: Int) :
|
||||||
|
PartialChangeProducer<HistoryListUiIntent, HistoryListPartialChange, HistoryListUiState> {
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
override fun toPartialChangeFlow(intentFlow: Flow<HistoryListUiIntent>): Flow<HistoryListPartialChange> =
|
||||||
|
merge(
|
||||||
|
intentFlow.filterIsInstance<HistoryListUiIntent.Refresh>()
|
||||||
|
.flatMapConcat { produceRefreshPartialChange() },
|
||||||
|
intentFlow.filterIsInstance<HistoryListUiIntent.LoadMore>()
|
||||||
|
.flatMapConcat { it.producePartialChange() },
|
||||||
|
intentFlow.filterIsInstance<HistoryListUiIntent.Delete>()
|
||||||
|
.flatMapConcat { it.producePartialChange() },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun produceRefreshPartialChange() =
|
||||||
|
HistoryUtil.getFlow(type, 0)
|
||||||
|
.map<List<History>, HistoryListPartialChange.Refresh> { histories ->
|
||||||
|
HistoryListPartialChange.Refresh.Success(
|
||||||
|
histories.filter { DateTimeUtils.isToday(it.timestamp) },
|
||||||
|
histories.filterNot { DateTimeUtils.isToday(it.timestamp) },
|
||||||
|
histories.size == HistoryUtil.PAGE_SIZE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.catch { HistoryListPartialChange.Refresh.Failure(it) }
|
||||||
|
|
||||||
|
private fun HistoryListUiIntent.LoadMore.producePartialChange() =
|
||||||
|
HistoryUtil.getFlow(type, page)
|
||||||
|
.map<List<History>, HistoryListPartialChange.LoadMore> { histories ->
|
||||||
|
HistoryListPartialChange.LoadMore.Success(
|
||||||
|
histories.filter { DateTimeUtils.isToday(it.timestamp) },
|
||||||
|
histories.filterNot { DateTimeUtils.isToday(it.timestamp) },
|
||||||
|
histories.size == HistoryUtil.PAGE_SIZE,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onStart { HistoryListPartialChange.LoadMore.Start }
|
||||||
|
.catch { HistoryListPartialChange.LoadMore.Failure(it) }
|
||||||
|
|
||||||
|
private fun HistoryListUiIntent.Delete.producePartialChange() =
|
||||||
|
flow { emit(LitePal.deleteAll<History>("id = ?", "$id")) }
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.map {
|
||||||
|
if (it > 0) HistoryListPartialChange.Delete.Success(id)
|
||||||
|
else HistoryListPartialChange.Delete.Failure(IllegalStateException("未知错误"))
|
||||||
|
}
|
||||||
|
.catch { emit(HistoryListPartialChange.Delete.Failure(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface HistoryListUiIntent : UiIntent {
|
||||||
|
object Refresh : HistoryListUiIntent
|
||||||
|
|
||||||
|
data class LoadMore(val page: Int) : HistoryListUiIntent
|
||||||
|
|
||||||
|
data class Delete(val id: Int) : HistoryListUiIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface HistoryListPartialChange : PartialChange<HistoryListUiState> {
|
||||||
|
sealed class Refresh : HistoryListPartialChange {
|
||||||
|
override fun reduce(oldState: HistoryListUiState): HistoryListUiState = when (this) {
|
||||||
|
is Failure -> oldState
|
||||||
|
is Success -> oldState.copy(
|
||||||
|
todayHistoryData = todayHistoryData,
|
||||||
|
beforeHistoryData = beforeHistoryData,
|
||||||
|
currentPage = 0,
|
||||||
|
hasMore = hasMore
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Success(
|
||||||
|
val todayHistoryData: List<History>,
|
||||||
|
val beforeHistoryData: List<History>,
|
||||||
|
val hasMore: Boolean
|
||||||
|
) : Refresh()
|
||||||
|
|
||||||
|
data class Failure(
|
||||||
|
val error: Throwable
|
||||||
|
) : Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class LoadMore : HistoryListPartialChange {
|
||||||
|
override fun reduce(oldState: HistoryListUiState): HistoryListUiState = when (this) {
|
||||||
|
is Failure -> oldState.copy(isLoadingMore = false)
|
||||||
|
Start -> oldState.copy(isLoadingMore = true)
|
||||||
|
is Success -> oldState.copy(
|
||||||
|
isLoadingMore = false,
|
||||||
|
todayHistoryData = oldState.todayHistoryData + todayHistoryData,
|
||||||
|
beforeHistoryData = oldState.beforeHistoryData + beforeHistoryData,
|
||||||
|
currentPage = currentPage,
|
||||||
|
hasMore = hasMore
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Start : LoadMore()
|
||||||
|
|
||||||
|
data class Success(
|
||||||
|
val todayHistoryData: List<History>,
|
||||||
|
val beforeHistoryData: List<History>,
|
||||||
|
val hasMore: Boolean,
|
||||||
|
val currentPage: Int
|
||||||
|
) : LoadMore()
|
||||||
|
|
||||||
|
data class Failure(
|
||||||
|
val error: Throwable
|
||||||
|
) : LoadMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Delete : HistoryListPartialChange {
|
||||||
|
override fun reduce(oldState: HistoryListUiState): HistoryListUiState = when (this) {
|
||||||
|
is Failure -> oldState
|
||||||
|
is Success -> oldState.copy(
|
||||||
|
todayHistoryData = oldState.todayHistoryData.filterNot { it.id == id },
|
||||||
|
beforeHistoryData = oldState.beforeHistoryData.filterNot { it.id == id })
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Success(
|
||||||
|
val id: Int
|
||||||
|
) : Delete()
|
||||||
|
|
||||||
|
data class Failure(
|
||||||
|
val error: Throwable
|
||||||
|
) : Delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HistoryListUiState(
|
||||||
|
val isRefreshing: Boolean = false,
|
||||||
|
val isLoadingMore: Boolean = false,
|
||||||
|
val hasMore: Boolean = true,
|
||||||
|
val currentPage: Int = 0,
|
||||||
|
val todayHistoryData: List<History> = emptyList(),
|
||||||
|
val beforeHistoryData: List<History> = emptyList(),
|
||||||
|
) : UiState
|
||||||
|
|
||||||
|
sealed interface HistoryListUiEvent : UiEvent {
|
||||||
|
sealed interface Delete : HistoryListUiEvent {
|
||||||
|
object Success : Delete
|
||||||
|
|
||||||
|
data class Failure(
|
||||||
|
val errorMsg: String
|
||||||
|
) : Delete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,6 @@ import androidx.compose.ui.unit.sp
|
||||||
import com.google.accompanist.placeholder.placeholder
|
import com.google.accompanist.placeholder.placeholder
|
||||||
import com.huanchengfly.tieba.post.R
|
import com.huanchengfly.tieba.post.R
|
||||||
import com.huanchengfly.tieba.post.activities.AppThemeActivity
|
import com.huanchengfly.tieba.post.activities.AppThemeActivity
|
||||||
import com.huanchengfly.tieba.post.activities.HistoryActivity
|
|
||||||
import com.huanchengfly.tieba.post.activities.UserActivity
|
import com.huanchengfly.tieba.post.activities.UserActivity
|
||||||
import com.huanchengfly.tieba.post.activities.WebViewActivity
|
import com.huanchengfly.tieba.post.activities.WebViewActivity
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
|
|
@ -57,6 +56,7 @@ import com.huanchengfly.tieba.post.models.database.Account
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.AboutPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.AboutPageDestination
|
||||||
|
import com.huanchengfly.tieba.post.ui.page.destinations.HistoryPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.SettingsPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.SettingsPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadStorePageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadStorePageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
|
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
|
||||||
|
|
@ -319,7 +319,7 @@ fun UserPage(
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.ic_outline_watch_later_24),
|
icon = ImageVector.vectorResource(id = R.drawable.ic_outline_watch_later_24),
|
||||||
text = stringResource(id = R.string.title_history),
|
text = stringResource(id = R.string.title_history),
|
||||||
onClick = {
|
onClick = {
|
||||||
context.goToActivity<HistoryActivity>()
|
navigator.navigate(HistoryPageDestination)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
ListMenuItem(
|
ListMenuItem(
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
package com.huanchengfly.tieba.post.utils;
|
|
||||||
|
|
||||||
import com.huanchengfly.tieba.post.models.database.History;
|
|
||||||
|
|
||||||
import org.litepal.LitePal;
|
|
||||||
import org.litepal.crud.async.FindMultiExecutor;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class HistoryUtil {
|
|
||||||
public static final int TYPE_FORUM = 1;
|
|
||||||
public static final int TYPE_THREAD = 2;
|
|
||||||
|
|
||||||
private HistoryUtil() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void deleteAll() {
|
|
||||||
LitePal.deleteAll(History.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeHistory(History history) {
|
|
||||||
writeHistory(history, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeHistory(History history, boolean async) {
|
|
||||||
add(history, async);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<History> getAll() {
|
|
||||||
return LitePal.order("timestamp desc, count desc").limit(100).find(History.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<History> getAll(int type) {
|
|
||||||
return LitePal.order("timestamp desc, count desc").where("type = ?", String.valueOf(type)).limit(100).find(History.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FindMultiExecutor<History> getAllAsync(int type) {
|
|
||||||
return LitePal.order("timestamp desc, count desc").where("type = ?", String.valueOf(type)).limit(100).findAsync(History.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean update(History history) {
|
|
||||||
History historyBean = LitePal.where("data = ?", history.getData()).findFirst(History.class);
|
|
||||||
if (historyBean != null) {
|
|
||||||
historyBean.setTimestamp(System.currentTimeMillis())
|
|
||||||
.setTitle(history.getTitle())
|
|
||||||
.setExtras(history.getExtras())
|
|
||||||
.setAvatar(history.getAvatar())
|
|
||||||
.setUsername(history.getUsername())
|
|
||||||
.setCount(historyBean.getCount() + 1)
|
|
||||||
.update(historyBean.getId());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void add(History history, boolean async) {
|
|
||||||
if (update(history)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
history.setCount(1)
|
|
||||||
.setTimestamp(System.currentTimeMillis());
|
|
||||||
if (async) {
|
|
||||||
history.saveAsync().listen(null);
|
|
||||||
} else {
|
|
||||||
history.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void add(History history) {
|
|
||||||
add(history, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.huanchengfly.tieba.post.utils
|
||||||
|
|
||||||
|
import com.huanchengfly.tieba.post.models.database.History
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import org.litepal.LitePal.deleteAll
|
||||||
|
import org.litepal.LitePal.order
|
||||||
|
import org.litepal.LitePal.where
|
||||||
|
import org.litepal.crud.async.FindMultiExecutor
|
||||||
|
import org.litepal.extension.find
|
||||||
|
|
||||||
|
object HistoryUtil {
|
||||||
|
const val PAGE_SIZE = 100
|
||||||
|
const val TYPE_FORUM = 1
|
||||||
|
const val TYPE_THREAD = 2
|
||||||
|
fun deleteAll() {
|
||||||
|
deleteAll(History::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun writeHistory(history: History, async: Boolean = false) {
|
||||||
|
add(history, async)
|
||||||
|
}
|
||||||
|
|
||||||
|
val all: List<History>
|
||||||
|
get() = order("timestamp desc, count desc").limit(100).find(
|
||||||
|
History::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getAll(type: Int): List<History> {
|
||||||
|
return order("timestamp desc, count desc").where("type = ?", type.toString())
|
||||||
|
.limit(PAGE_SIZE)
|
||||||
|
.find(
|
||||||
|
History::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllAsync(type: Int): FindMultiExecutor<History> {
|
||||||
|
return order("timestamp desc, count desc").where("type = ?", type.toString())
|
||||||
|
.limit(PAGE_SIZE)
|
||||||
|
.findAsync(
|
||||||
|
History::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFlow(
|
||||||
|
type: Int,
|
||||||
|
page: Int
|
||||||
|
): Flow<List<History>> {
|
||||||
|
return flow {
|
||||||
|
delay(100)
|
||||||
|
emit(
|
||||||
|
where("type = ?", "$type")
|
||||||
|
.order("timestamp desc, count desc")
|
||||||
|
.limit(PAGE_SIZE)
|
||||||
|
.offset(page * 100)
|
||||||
|
.find<History>()
|
||||||
|
)
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun update(history: History): Boolean {
|
||||||
|
val historyBean = where("data = ?", history.data).findFirst(
|
||||||
|
History::class.java
|
||||||
|
)
|
||||||
|
if (historyBean != null) {
|
||||||
|
historyBean.setTimestamp(System.currentTimeMillis())
|
||||||
|
.setTitle(history.title)
|
||||||
|
.setExtras(history.extras)
|
||||||
|
.setAvatar(history.avatar)
|
||||||
|
.setUsername(history.username)
|
||||||
|
.setCount(historyBean.count + 1)
|
||||||
|
.update(historyBean.id.toLong())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun add(history: History, async: Boolean = false) {
|
||||||
|
if (update(history)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
history.setCount(1).timestamp = System.currentTimeMillis()
|
||||||
|
if (async) {
|
||||||
|
history.saveAsync().listen(null)
|
||||||
|
} else {
|
||||||
|
history.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -626,4 +626,6 @@
|
||||||
<string name="sort_menu">排序菜单</string>
|
<string name="sort_menu">排序菜单</string>
|
||||||
<string name="delete_store_success">取消收藏成功</string>
|
<string name="delete_store_success">取消收藏成功</string>
|
||||||
<string name="delete_store_failure">取消收藏失败 %s</string>
|
<string name="delete_store_failure">取消收藏失败 %s</string>
|
||||||
|
<string name="delete_history_failure">删除历史记录失败 %s</string>
|
||||||
|
<string name="delete_history_success">删除历史记录成功</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue