Improve ArticleList and add SwipeRefresh and WebView loading
Fix palettes not selected when first launch
This commit is contained in:
parent
c621f7d794
commit
7f3f5482eb
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
31
app/src/main/java/me/ash/reader/ui/component/SwipeRefresh.kt
Normal file
31
app/src/main/java/me/ash/reader/ui/component/SwipeRefresh.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,4 +266,20 @@ 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 "",
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -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(
|
||||||
|
@ -175,4 +208,9 @@ enum class MarkAsReadBefore {
|
||||||
ThreeDays,
|
ThreeDays,
|
||||||
OneDay,
|
OneDay,
|
||||||
All,
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class FlowItemView {
|
||||||
|
class Article(val articleWithFeed: ArticleWithFeed) : FlowItemView()
|
||||||
|
class Date(val date: String) : FlowItemView()
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user