Improve ArticleList and add SwipeRefresh and WebView loading

Fix palettes not selected when first launch
This commit is contained in:
Ash 2022-04-19 19:55:18 +08:00
parent c621f7d794
commit 7f3f5482eb
12 changed files with 275 additions and 187 deletions

View File

@ -2,6 +2,8 @@ package me.ash.reader.data.repository
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.ui.ext.formatAsString
import java.util.*
import javax.inject.Inject
class StringsRepository @Inject constructor(
@ -9,4 +11,5 @@ class StringsRepository @Inject constructor(
private val context: Context,
) {
fun getString(resId: Int) = context.getString(resId)
fun formatAsString(date: Date?) = date?.formatAsString(context)
}

View File

@ -0,0 +1,31 @@
package me.ash.reader.ui.component
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import com.google.accompanist.swiperefresh.SwipeRefreshIndicator
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import me.ash.reader.ui.theme.palette.onDark
@Composable
fun SwipeRefresh(
isRefresh: Boolean = false,
onRefresh: () -> Unit = {},
content: @Composable () -> Unit = {},
) {
com.google.accompanist.swiperefresh.SwipeRefresh(
state = rememberSwipeRefreshState(isRefresh),
onRefresh = onRefresh,
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
fade = true,
scale = true,
contentColor = MaterialTheme.colorScheme.primary,
backgroundColor = MaterialTheme.colorScheme.surface onDark MaterialTheme.colorScheme.surfaceVariant,
)
}
) {
content()
}
}

View File

@ -1,7 +1,6 @@
package me.ash.reader.ui.component
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.net.http.SslError
import android.util.Log
@ -15,9 +14,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel
import me.ash.reader.ui.page.home.read.ReadViewAction
import me.ash.reader.ui.page.home.read.ReadViewModel
const val INJECTION_TOKEN = "/android_asset_font/"
@ -25,8 +21,6 @@ const val INJECTION_TOKEN = "/android_asset_font/"
fun WebView(
modifier: Modifier = Modifier,
content: String,
viewModel: ReadViewModel = hiltViewModel(),
onProgressChange: (progress: Int) -> Unit = {},
onReceivedError: (error: WebResourceError?) -> Unit = {}
) {
val context = LocalContext.current
@ -57,16 +51,6 @@ fun WebView(
return super.shouldInterceptRequest(view, url);
}
override fun onPageStarted(
view: WebView?,
url: String?,
favicon: Bitmap?
) {
super.onPageStarted(view, url, favicon)
// _isLoading = true
onProgressChange(-1)
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
val jsCode = "javascript:(function(){" +
@ -78,8 +62,6 @@ fun WebView(
"alert('asf');" +
"}}})()"
view!!.loadUrl(jsCode)
viewModel.dispatch(ReadViewAction.ChangeLoading(false))
onProgressChange(100)
}
override fun shouldOverrideUrlLoading(
@ -173,10 +155,12 @@ fun getStyle(argb: Int): String = """
padding: 0 24px;
}
img {
img, video {
margin: 0 -24px 20px;
width: calc(100% + 48px);
height: auto;
border-top: 1px solid ${argbToCssColor(argb)}08;
border-bottom: 1px solid ${argbToCssColor(argb)}08;
}
p,span,a,ol,ul,blockquote,article,section {

View File

@ -12,7 +12,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.launch
import me.ash.reader.ui.component.ViewPager
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.findActivity
@ -49,25 +48,12 @@ fun HomePage(
LaunchedEffect(openArticleId) {
if (openArticleId.isNotEmpty()) {
readViewModel.dispatch(ReadViewAction.ScrollToItem(2))
launch {
val article = readViewModel
.rssRepository.get()
.findArticleById(openArticleId) ?: return@launch
readViewModel.dispatch(ReadViewAction.InitData(article))
if (article.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
homeViewModel.dispatch(
HomeViewAction.ScrollToPage(
scope = scope,
targetPage = 2,
)
)
readViewModel.dispatch(ReadViewAction.InitData(openArticleId))
readViewModel.dispatch(ReadViewAction.ScrollToItem(0))
homeViewModel.dispatch(HomeViewAction.ScrollToPage(scope, 2))
openArticleId = ""
}
}
}
BackHandler(true) {
val currentPage = viewState.pagerState.currentPage
@ -142,7 +128,7 @@ fun HomePage(
},
onItemClick = {
readViewModel.dispatch(ReadViewAction.ScrollToItem(0))
readViewModel.dispatch(ReadViewAction.InitData(it))
readViewModel.dispatch(ReadViewAction.InitData(it.article.id))
if (it.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)

View File

@ -22,7 +22,7 @@ import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val rssRepository: RssRepository,
private val workManager: WorkManager,
workManager: WorkManager,
) : ViewModel() {
private val _viewState = MutableStateFlow(HomeViewState())

View File

@ -1,6 +1,5 @@
package me.ash.reader.ui.page.home.flow
import android.content.Context
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@ -9,33 +8,31 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.ext.formatAsString
@Suppress("FunctionName")
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.generateArticleList(
context: Context,
pagingItems: LazyPagingItems<ArticleWithFeed>,
fun LazyListScope.ArticleList(
pagingItems: LazyPagingItems<FlowItemView>,
onClick: (ArticleWithFeed) -> Unit = {},
) {
var lastItemDay: String? = null
for (itemIndex in 0 until pagingItems.itemCount) {
val currentItem = pagingItems.peek(itemIndex) ?: continue
val currentItemDay = currentItem.article.date.formatAsString(context)
if (lastItemDay != currentItemDay) {
if (itemIndex != 0) {
item { Spacer(modifier = Modifier.height(40.dp)) }
}
stickyHeader {
StickyHeader(currentItemDay)
}
}
when (val item = pagingItems[itemIndex]) {
is FlowItemView.Article -> {
item {
ArticleItem(
articleWithFeed = pagingItems[itemIndex] ?: return@item,
articleWithFeed = item.articleWithFeed,
) {
onClick(it)
}
}
lastItemDay = currentItemDay
}
is FlowItemView.Date -> {
if (itemIndex != 0) item { Spacer(modifier = Modifier.height(40.dp)) }
stickyHeader {
StickyHeader(item.date)
}
}
else -> {}
}
}
}

View File

@ -17,7 +17,6 @@ import androidx.compose.material3.SmallTopAppBar
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
@ -35,6 +34,7 @@ import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton
import me.ash.reader.ui.component.SwipeRefresh
import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.page.home.FilterBar
@ -56,7 +56,6 @@ fun FlowPage(
onScrollToPage: (targetPage: Int) -> Unit = {},
onItemClick: (item: ArticleWithFeed) -> Unit = {},
) {
val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }
val scope = rememberCoroutineScope()
@ -168,20 +167,19 @@ fun FlowPage(
// url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json",
// )
// }
SwipeRefresh(
onRefresh = {
if (!isSyncing) {
flowViewModel.dispatch(FlowViewAction.Sync)
}
}
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = viewState.listState,
) {
item {
DisplayText(
modifier = Modifier.padding(start = 30.dp),
text = when {
filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name
else -> filterState.filter.getName()
},
desc = if (isSyncing) stringResource(R.string.syncing) else "",
)
DisplayTextHeader(filterState, isSyncing)
AnimatedVisibility(
visible = markAsRead,
enter = fadeIn() + expandVertically(),
@ -241,8 +239,7 @@ fun FlowPage(
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
}
}
generateArticleList(
context = context,
ArticleList(
pagingItems = pagingItems,
) {
onSearch = false
@ -255,6 +252,7 @@ fun FlowPage(
}
}
}
}
},
bottomBar = {
FilterBar(
@ -269,3 +267,19 @@ fun FlowPage(
}
)
}
@Composable
private fun DisplayTextHeader(
filterState: FilterState,
isSyncing: Boolean
) {
DisplayText(
modifier = Modifier.padding(start = 30.dp),
text = when {
filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name
else -> filterState.filter.getName()
},
desc = if (isSyncing) stringResource(R.string.syncing) else "",
)
}

View File

@ -3,16 +3,13 @@ package me.ash.reader.ui.page.home.flow
import androidx.compose.foundation.lazy.LazyListState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.*
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.ui.page.home.FilterState
import java.util.*
import javax.inject.Inject
@ -20,14 +17,16 @@ import javax.inject.Inject
@HiltViewModel
class FlowViewModel @Inject constructor(
private val rssRepository: RssRepository,
private val stringsRepository: StringsRepository,
) : ViewModel() {
private val _viewState = MutableStateFlow(ArticleViewState())
val viewState: StateFlow<ArticleViewState> = _viewState.asStateFlow()
fun dispatch(action: FlowViewAction) {
when (action) {
is FlowViewAction.Sync -> sync()
is FlowViewAction.FetchData -> fetchData(action.filterState)
is FlowViewAction.ChangeRefreshing -> changeRefreshing(action.isRefreshing)
is FlowViewAction.ChangeIsBack -> changeIsBack(action.isBack)
is FlowViewAction.ScrollToItem -> scrollToItem(action.index)
is FlowViewAction.MarkAsRead -> markAsRead(
action.groupId,
@ -39,6 +38,10 @@ class FlowViewModel @Inject constructor(
}
}
private fun sync() {
rssRepository.get().doSync()
}
private fun fetchData(filterState: FilterState? = null) {
// viewModelScope.launch(Dispatchers.Default) {
// rssRepository.get().pullImportant(filterState.filter.isStarred(), true)
@ -62,7 +65,21 @@ class FlowViewModel @Inject constructor(
isStarred = _viewState.value.filterState?.filter?.isStarred() ?: false,
isUnread = _viewState.value.filterState?.filter?.isUnread() ?: false,
)
}.flow.flowOn(Dispatchers.IO).cachedIn(viewModelScope)
}.flow.map {
it.map {
FlowItemView.Article(it)
}.insertSeparators { before, after ->
val beforeDate =
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
val afterDate =
stringsRepository.formatAsString(after?.articleWithFeed?.article?.date)
if (beforeDate != afterDate) {
afterDate?.let { FlowItemView.Date(it) }
} else {
null
}
}
}.cachedIn(viewModelScope)
)
}
} else if (filterState != null) {
@ -76,7 +93,21 @@ class FlowViewModel @Inject constructor(
isStarred = filterState.filter.isStarred(),
isUnread = filterState.filter.isUnread(),
)
}.flow.flowOn(Dispatchers.IO).cachedIn(viewModelScope)
}.flow.map {
it.map {
FlowItemView.Article(it)
}.insertSeparators { before, after ->
val beforeDate =
stringsRepository.formatAsString(before?.articleWithFeed?.article?.date)
val afterDate =
stringsRepository.formatAsString(after?.articleWithFeed?.article?.date)
if (beforeDate != afterDate) {
afterDate?.let { FlowItemView.Date(it) }
} else {
null
}
}
}.cachedIn(viewModelScope)
)
}
}
@ -88,9 +119,9 @@ class FlowViewModel @Inject constructor(
}
}
private fun changeRefreshing(isRefreshing: Boolean) {
private fun changeIsBack(isBack: Boolean) {
_viewState.update {
it.copy(isRefreshing = isRefreshing)
it.copy(isBack = isBack)
}
}
@ -139,19 +170,21 @@ data class ArticleViewState(
val filterState: FilterState? = null,
val filterImportant: Int = 0,
val listState: LazyListState = LazyListState(),
val isRefreshing: Boolean = false,
val pagingData: Flow<PagingData<ArticleWithFeed>> = emptyFlow(),
val isBack: Boolean = false,
val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
val syncWorkInfo: String = "",
val searchContent: String = "",
)
sealed class FlowViewAction {
object Sync : FlowViewAction()
data class FetchData(
val filterState: FilterState,
) : FlowViewAction()
data class ChangeRefreshing(
val isRefreshing: Boolean
data class ChangeIsBack(
val isBack: Boolean
) : FlowViewAction()
data class ScrollToItem(
@ -176,3 +209,8 @@ enum class MarkAsReadBefore {
OneDay,
All,
}
sealed class FlowItemView {
class Article(val articleWithFeed: ArticleWithFeed) : FlowItemView()
class Date(val date: String) : FlowItemView()
}

View File

@ -20,7 +20,8 @@ fun StickyHeader(currentItemDay: String) {
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(start = if (true) 54.dp else 24.dp),
modifier = Modifier
.padding(start = if (true) 54.dp else 24.dp, bottom = 4.dp),
text = currentItemDay,
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge,

View File

@ -67,9 +67,6 @@ fun ReadPage(
if (it.article.isUnread) {
readViewModel.dispatch(ReadViewAction.MarkUnread(false))
}
if (it.feed.isFullContent) {
readViewModel.dispatch(ReadViewAction.RenderFullContent)
}
}
}
@ -96,6 +93,7 @@ fun ReadPage(
Content(
content = viewState.content ?: "",
articleWithFeed = viewState.articleWithFeed,
viewState = viewState,
LazyListState = viewState.listState,
)
Box(
@ -156,7 +154,9 @@ private fun TopBar(
actions = {
if (isShowActions) {
FeedbackIconButton(
modifier = Modifier.size(22.dp).alpha(0.5f),
modifier = Modifier
.size(22.dp)
.alpha(0.5f),
imageVector = Icons.Outlined.Headphones,
contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface,
@ -179,6 +179,7 @@ private fun TopBar(
private fun Content(
content: String,
articleWithFeed: ArticleWithFeed?,
viewState: ReadViewState,
LazyListState: LazyListState = rememberLazyListState(),
) {
Column {
@ -208,7 +209,27 @@ private fun Content(
}
item {
Spacer(modifier = Modifier.height(22.dp))
Crossfade(targetState = content) { content ->
AnimatedVisibility(
visible = viewState.isLoading,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column {
Spacer(modifier = Modifier.height(22.dp))
CircularProgressIndicator(
modifier = Modifier
.size(30.dp),
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(22.dp))
}
}
}
if (!viewState.isLoading) {
WebView(
content = content
)

View File

@ -26,7 +26,7 @@ class ReadViewModel @Inject constructor(
fun dispatch(action: ReadViewAction) {
when (action) {
is ReadViewAction.InitData -> bindArticleWithFeed(action.articleWithFeed)
is ReadViewAction.InitData -> bindArticleWithFeed(action.articleId)
is ReadViewAction.RenderDescriptionContent -> renderDescriptionContent()
is ReadViewAction.RenderFullContent -> renderFullContent()
is ReadViewAction.MarkUnread -> markUnread(action.isUnread)
@ -37,9 +37,17 @@ class ReadViewModel @Inject constructor(
}
}
private fun bindArticleWithFeed(articleWithFeed: ArticleWithFeed) {
private fun bindArticleWithFeed(articleId: String) {
changeLoading(true)
viewModelScope.launch {
_viewState.update {
it.copy(articleWithFeed = articleWithFeed)
it.copy(articleWithFeed = rssRepository.get().findArticleById(articleId))
}
_viewState.value.articleWithFeed?.let {
if (it.feed.isFullContent) internalRenderFullContent()
else renderDescriptionContent()
}
changeLoading(false)
}
}
@ -55,8 +63,13 @@ class ReadViewModel @Inject constructor(
}
private fun renderFullContent() {
changeLoading(true)
viewModelScope.launch {
internalRenderFullContent()
}
}
private suspend fun internalRenderFullContent() {
changeLoading(true)
try {
_viewState.update {
it.copy(
@ -74,7 +87,7 @@ class ReadViewModel @Inject constructor(
)
}
}
}
changeLoading(false)
}
private fun markUnread(isUnread: Boolean) {
@ -141,13 +154,13 @@ class ReadViewModel @Inject constructor(
data class ReadViewState(
val articleWithFeed: ArticleWithFeed? = null,
val content: String? = null,
val isLoading: Boolean = false,
val isLoading: Boolean = true,
val listState: LazyListState = LazyListState(),
)
sealed class ReadViewAction {
data class InitData(
val articleWithFeed: ArticleWithFeed,
val articleId: String,
) : ReadViewAction()
object RenderDescriptionContent : ReadViewAction()

View File

@ -191,8 +191,8 @@ fun Palettes(
val context = LocalContext.current
val scope = rememberCoroutineScope()
val themeIndex = context.dataStore.data
.map { it[DataStoreKeys.ThemeIndex.key] ?: 0 }
.collectAsState(initial = 0).value
.map { it[DataStoreKeys.ThemeIndex.key] ?: 5 }
.collectAsState(initial = 5).value
val customPrimaryColor = context.dataStore.data
.map { it[DataStoreKeys.CustomPrimaryColor.key] ?: "" }
.collectAsState(initial = "").value