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 android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import me.ash.reader.ui.ext.formatAsString
import java.util.*
import javax.inject.Inject import javax.inject.Inject
class StringsRepository @Inject constructor( class StringsRepository @Inject constructor(
@ -9,4 +11,5 @@ class StringsRepository @Inject constructor(
private val context: Context, private val context: Context,
) { ) {
fun getString(resId: Int) = context.getString(resId) 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 package me.ash.reader.ui.component
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.net.http.SslError import android.net.http.SslError
import android.util.Log import android.util.Log
@ -15,9 +14,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView 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/" const val INJECTION_TOKEN = "/android_asset_font/"
@ -25,8 +21,6 @@ const val INJECTION_TOKEN = "/android_asset_font/"
fun WebView( fun WebView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: String, content: String,
viewModel: ReadViewModel = hiltViewModel(),
onProgressChange: (progress: Int) -> Unit = {},
onReceivedError: (error: WebResourceError?) -> Unit = {} onReceivedError: (error: WebResourceError?) -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -57,16 +51,6 @@ fun WebView(
return super.shouldInterceptRequest(view, url); 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?) { override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url) super.onPageFinished(view, url)
val jsCode = "javascript:(function(){" + val jsCode = "javascript:(function(){" +
@ -78,8 +62,6 @@ fun WebView(
"alert('asf');" + "alert('asf');" +
"}}})()" "}}})()"
view!!.loadUrl(jsCode) view!!.loadUrl(jsCode)
viewModel.dispatch(ReadViewAction.ChangeLoading(false))
onProgressChange(100)
} }
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
@ -173,10 +155,12 @@ fun getStyle(argb: Int): String = """
padding: 0 24px; padding: 0 24px;
} }
img { img, video {
margin: 0 -24px 20px; margin: 0 -24px 20px;
width: calc(100% + 48px); width: calc(100% + 48px);
height: auto; height: auto;
border-top: 1px solid ${argbToCssColor(argb)}08;
border-bottom: 1px solid ${argbToCssColor(argb)}08;
} }
p,span,a,ol,ul,blockquote,article,section { 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 androidx.navigation.NavHostController
import com.google.accompanist.insets.statusBarsPadding import com.google.accompanist.insets.statusBarsPadding
import com.google.accompanist.pager.ExperimentalPagerApi import com.google.accompanist.pager.ExperimentalPagerApi
import kotlinx.coroutines.launch
import me.ash.reader.ui.component.ViewPager import me.ash.reader.ui.component.ViewPager
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.findActivity import me.ash.reader.ui.ext.findActivity
@ -49,23 +48,10 @@ fun HomePage(
LaunchedEffect(openArticleId) { LaunchedEffect(openArticleId) {
if (openArticleId.isNotEmpty()) { if (openArticleId.isNotEmpty()) {
readViewModel.dispatch(ReadViewAction.ScrollToItem(2)) readViewModel.dispatch(ReadViewAction.InitData(openArticleId))
launch { readViewModel.dispatch(ReadViewAction.ScrollToItem(0))
val article = readViewModel homeViewModel.dispatch(HomeViewAction.ScrollToPage(scope, 2))
.rssRepository.get() openArticleId = ""
.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,
)
)
openArticleId = ""
}
} }
} }
@ -142,7 +128,7 @@ fun HomePage(
}, },
onItemClick = { onItemClick = {
readViewModel.dispatch(ReadViewAction.ScrollToItem(0)) 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) if (it.feed.isFullContent) readViewModel.dispatch(ReadViewAction.RenderFullContent)
else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) else readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)
readViewModel.dispatch(ReadViewAction.RenderDescriptionContent) readViewModel.dispatch(ReadViewAction.RenderDescriptionContent)

View File

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

View File

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

View File

@ -17,7 +17,6 @@ import androidx.compose.material3.SmallTopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource 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.data.repository.SyncWorker.Companion.getIsSyncing
import me.ash.reader.ui.component.DisplayText import me.ash.reader.ui.component.DisplayText
import me.ash.reader.ui.component.FeedbackIconButton 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.collectAsStateValue
import me.ash.reader.ui.ext.getName import me.ash.reader.ui.ext.getName
import me.ash.reader.ui.page.home.FilterBar import me.ash.reader.ui.page.home.FilterBar
@ -56,7 +56,6 @@ fun FlowPage(
onScrollToPage: (targetPage: Int) -> Unit = {}, onScrollToPage: (targetPage: Int) -> Unit = {},
onItemClick: (item: ArticleWithFeed) -> Unit = {}, onItemClick: (item: ArticleWithFeed) -> Unit = {},
) { ) {
val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -168,90 +167,89 @@ fun FlowPage(
// url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json", // url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json",
// ) // )
// } // }
LazyColumn( SwipeRefresh(
modifier = Modifier.fillMaxSize(), onRefresh = {
state = viewState.listState, if (!isSyncing) {
flowViewModel.dispatch(FlowViewAction.Sync)
}
}
) { ) {
item { LazyColumn(
DisplayText( modifier = Modifier.fillMaxSize(),
modifier = Modifier.padding(start = 30.dp), state = viewState.listState,
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 "",
)
AnimatedVisibility(
visible = markAsRead,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
}
MarkAsReadBar(
visible = markAsRead,
absoluteY = if (isSyncing) (4 + 16 + 180).dp else 180.dp,
onDismissRequest = {
markAsRead = false
},
) {
markAsRead = false
flowViewModel.dispatch(
FlowViewAction.MarkAsRead(
groupId = filterState.group?.id,
feedId = filterState.feed?.id,
articleId = null,
markAsReadBefore = it,
)
)
}
AnimatedVisibility(
visible = onSearch,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
SearchBar(
value = viewState.searchContent,
placeholder = when {
filterState.group != null -> stringResource(
R.string.search_for_in,
filterState.filter.getName(),
filterState.group.name
)
filterState.feed != null -> stringResource(
R.string.search_for_in,
filterState.filter.getName(),
filterState.feed.name
)
else -> stringResource(
R.string.search_for,
filterState.filter.getName()
)
},
focusRequester = focusRequester,
onValueChange = {
flowViewModel.dispatch(FlowViewAction.InputSearchContent(it))
},
onClose = {
onSearch = false
flowViewModel.dispatch(FlowViewAction.InputSearchContent(""))
}
)
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
}
}
generateArticleList(
context = context,
pagingItems = pagingItems,
) { ) {
onSearch = false item {
onItemClick(it) DisplayTextHeader(filterState, isSyncing)
} AnimatedVisibility(
item { visible = markAsRead,
Spacer(modifier = Modifier.height(64.dp)) enter = fadeIn() + expandVertically(),
if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount != 0) { exit = fadeOut() + shrinkVertically(),
) {
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
}
MarkAsReadBar(
visible = markAsRead,
absoluteY = if (isSyncing) (4 + 16 + 180).dp else 180.dp,
onDismissRequest = {
markAsRead = false
},
) {
markAsRead = false
flowViewModel.dispatch(
FlowViewAction.MarkAsRead(
groupId = filterState.group?.id,
feedId = filterState.feed?.id,
articleId = null,
markAsReadBefore = it,
)
)
}
AnimatedVisibility(
visible = onSearch,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
SearchBar(
value = viewState.searchContent,
placeholder = when {
filterState.group != null -> stringResource(
R.string.search_for_in,
filterState.filter.getName(),
filterState.group.name
)
filterState.feed != null -> stringResource(
R.string.search_for_in,
filterState.filter.getName(),
filterState.feed.name
)
else -> stringResource(
R.string.search_for,
filterState.filter.getName()
)
},
focusRequester = focusRequester,
onValueChange = {
flowViewModel.dispatch(FlowViewAction.InputSearchContent(it))
},
onClose = {
onSearch = false
flowViewModel.dispatch(FlowViewAction.InputSearchContent(""))
}
)
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
}
}
ArticleList(
pagingItems = pagingItems,
) {
onSearch = false
onItemClick(it)
}
item {
Spacer(modifier = Modifier.height(64.dp)) Spacer(modifier = Modifier.height(64.dp))
if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount != 0) {
Spacer(modifier = Modifier.height(64.dp))
}
} }
} }
} }
@ -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.compose.foundation.lazy.LazyListState
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.Pager import androidx.paging.*
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.data.repository.RssRepository import me.ash.reader.data.repository.RssRepository
import me.ash.reader.data.repository.StringsRepository
import me.ash.reader.ui.page.home.FilterState import me.ash.reader.ui.page.home.FilterState
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -20,14 +17,16 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FlowViewModel @Inject constructor( class FlowViewModel @Inject constructor(
private val rssRepository: RssRepository, private val rssRepository: RssRepository,
private val stringsRepository: StringsRepository,
) : ViewModel() { ) : ViewModel() {
private val _viewState = MutableStateFlow(ArticleViewState()) private val _viewState = MutableStateFlow(ArticleViewState())
val viewState: StateFlow<ArticleViewState> = _viewState.asStateFlow() val viewState: StateFlow<ArticleViewState> = _viewState.asStateFlow()
fun dispatch(action: FlowViewAction) { fun dispatch(action: FlowViewAction) {
when (action) { when (action) {
is FlowViewAction.Sync -> sync()
is FlowViewAction.FetchData -> fetchData(action.filterState) 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.ScrollToItem -> scrollToItem(action.index)
is FlowViewAction.MarkAsRead -> markAsRead( is FlowViewAction.MarkAsRead -> markAsRead(
action.groupId, action.groupId,
@ -39,6 +38,10 @@ class FlowViewModel @Inject constructor(
} }
} }
private fun sync() {
rssRepository.get().doSync()
}
private fun fetchData(filterState: FilterState? = null) { private fun fetchData(filterState: FilterState? = null) {
// viewModelScope.launch(Dispatchers.Default) { // viewModelScope.launch(Dispatchers.Default) {
// rssRepository.get().pullImportant(filterState.filter.isStarred(), true) // rssRepository.get().pullImportant(filterState.filter.isStarred(), true)
@ -62,7 +65,21 @@ class FlowViewModel @Inject constructor(
isStarred = _viewState.value.filterState?.filter?.isStarred() ?: false, isStarred = _viewState.value.filterState?.filter?.isStarred() ?: false,
isUnread = _viewState.value.filterState?.filter?.isUnread() ?: 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) { } else if (filterState != null) {
@ -76,7 +93,21 @@ class FlowViewModel @Inject constructor(
isStarred = filterState.filter.isStarred(), isStarred = filterState.filter.isStarred(),
isUnread = filterState.filter.isUnread(), 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 { _viewState.update {
it.copy(isRefreshing = isRefreshing) it.copy(isBack = isBack)
} }
} }
@ -139,19 +170,21 @@ data class ArticleViewState(
val filterState: FilterState? = null, val filterState: FilterState? = null,
val filterImportant: Int = 0, val filterImportant: Int = 0,
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val isRefreshing: Boolean = false, val isBack: Boolean = false,
val pagingData: Flow<PagingData<ArticleWithFeed>> = emptyFlow(), val pagingData: Flow<PagingData<FlowItemView>> = emptyFlow(),
val syncWorkInfo: String = "", val syncWorkInfo: String = "",
val searchContent: String = "", val searchContent: String = "",
) )
sealed class FlowViewAction { sealed class FlowViewAction {
object Sync : FlowViewAction()
data class FetchData( data class FetchData(
val filterState: FilterState, val filterState: FilterState,
) : FlowViewAction() ) : FlowViewAction()
data class ChangeRefreshing( data class ChangeIsBack(
val isRefreshing: Boolean val isBack: Boolean
) : FlowViewAction() ) : FlowViewAction()
data class ScrollToItem( data class ScrollToItem(
@ -176,3 +209,8 @@ enum class MarkAsReadBefore {
OneDay, OneDay,
All, 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 verticalAlignment = Alignment.CenterVertically
) { ) {
Text( 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, text = currentItemDay,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,

View File

@ -67,9 +67,6 @@ fun ReadPage(
if (it.article.isUnread) { if (it.article.isUnread) {
readViewModel.dispatch(ReadViewAction.MarkUnread(false)) readViewModel.dispatch(ReadViewAction.MarkUnread(false))
} }
if (it.feed.isFullContent) {
readViewModel.dispatch(ReadViewAction.RenderFullContent)
}
} }
} }
@ -96,6 +93,7 @@ fun ReadPage(
Content( Content(
content = viewState.content ?: "", content = viewState.content ?: "",
articleWithFeed = viewState.articleWithFeed, articleWithFeed = viewState.articleWithFeed,
viewState = viewState,
LazyListState = viewState.listState, LazyListState = viewState.listState,
) )
Box( Box(
@ -156,7 +154,9 @@ private fun TopBar(
actions = { actions = {
if (isShowActions) { if (isShowActions) {
FeedbackIconButton( FeedbackIconButton(
modifier = Modifier.size(22.dp).alpha(0.5f), modifier = Modifier
.size(22.dp)
.alpha(0.5f),
imageVector = Icons.Outlined.Headphones, imageVector = Icons.Outlined.Headphones,
contentDescription = stringResource(R.string.mark_all_as_read), contentDescription = stringResource(R.string.mark_all_as_read),
tint = MaterialTheme.colorScheme.onSurface, tint = MaterialTheme.colorScheme.onSurface,
@ -179,6 +179,7 @@ private fun TopBar(
private fun Content( private fun Content(
content: String, content: String,
articleWithFeed: ArticleWithFeed?, articleWithFeed: ArticleWithFeed?,
viewState: ReadViewState,
LazyListState: LazyListState = rememberLazyListState(), LazyListState: LazyListState = rememberLazyListState(),
) { ) {
Column { Column {
@ -208,7 +209,27 @@ private fun Content(
} }
item { item {
Spacer(modifier = Modifier.height(22.dp)) 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( WebView(
content = content content = content
) )

View File

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

View File

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