feat(ui): ArticleListReaderPage

This commit is contained in:
junkfood 2024-09-26 06:25:53 +08:00
parent d1f9641976
commit aca2028f5d
No known key found for this signature in database
GPG Key ID: 2EA5B648DB112A34
9 changed files with 152 additions and 51 deletions

View File

@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.aboutlibraries)
alias(libs.plugins.room)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.parcelize)
}
fun fetchGitCommitHash(): String {
@ -131,6 +132,9 @@ dependencies {
implementation(libs.compose.ui.tooling.preview)
androidTestImplementation(libs.compose.ui.test.junit4)
implementation(libs.compose.material3)
implementation(libs.compose.material3.adaptive)
implementation(libs.compose.material3.adaptive.layout)
implementation(libs.compose.material3.adaptive.navigation)
// Accompanist
implementation(libs.accompanist.swiperefresh)

View File

@ -28,6 +28,7 @@ import me.ash.reader.ui.ext.initialFilter
import me.ash.reader.ui.ext.initialPage
import me.ash.reader.ui.ext.isFirstLaunch
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.adaptive.ArticleListReaderPage
import me.ash.reader.ui.page.home.feeds.FeedsPage
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import me.ash.reader.ui.page.home.flow.FlowPage
@ -164,13 +165,37 @@ fun HomeEntry(
)
}
animatedComposable(route = RouteName.FLOW) {
FlowPage(
ArticleListReaderPage(
modifier = Modifier,
navController = navController,
homeViewModel = homeViewModel,
homeViewModel = homeViewModel
)
/* FlowPage(
homeViewModel = homeViewModel,
onNavigateToFeeds = {
if (navController.previousBackStackEntry == null) {
navController.navigate(RouteName.FEEDS) {
launchSingleTop = true
}
} else {
navController.popBackStack()
}
}, onOpenArticle = {
navController.navigate("${RouteName.READING}/${it}") {
launchSingleTop = true
}
}
)*/
}
animatedComposable(route = "${RouteName.READING}/{articleId}") {
ReadingPage(navController = navController, homeViewModel = homeViewModel)
val articleId = it.arguments?.getString("articleId")
ReadingPage(
navController = navController,
articleId = articleId,
homeViewModel = homeViewModel
)
}
// Settings

View File

@ -0,0 +1,77 @@
package me.ash.reader.ui.page.home.adaptive
import android.os.Parcelable
import androidx.activity.compose.BackHandler
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.AnimatedPane
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import kotlinx.parcelize.Parcelize
import me.ash.reader.ui.page.common.RouteName
import me.ash.reader.ui.page.home.HomeViewModel
import me.ash.reader.ui.page.home.flow.FlowPage
import me.ash.reader.ui.page.home.flow.FlowViewModel
import me.ash.reader.ui.page.home.reading.ReadingPage
import me.ash.reader.ui.page.home.reading.ReadingViewModel
@Parcelize
data class ArticleData(val id: String) : Parcelable
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ArticleListReaderPage(
modifier: Modifier = Modifier,
navController: NavHostController,
flowViewModel: FlowViewModel = hiltViewModel(),
readingViewModel: ReadingViewModel = hiltViewModel(),
homeViewModel: HomeViewModel,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<ArticleData>()
BackHandler(navigator.canNavigateBack()) {
navigator.navigateBack()
}
val currentArticle = navigator.currentDestination?.content
ListDetailPaneScaffold(
modifier = modifier,
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
AnimatedPane {
FlowPage(
homeViewModel = homeViewModel,
flowViewModel = flowViewModel,
readingArticleId = currentArticle?.id,
onNavigateToFeeds = {
if (navController.previousBackStackEntry == null) {
navController.navigate(RouteName.FEEDS) {
launchSingleTop = true
}
} else {
navController.popBackStack()
}
}, onOpenArticle = {
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, ArticleData(it))
}
)
}
},
detailPane = {
AnimatedPane {
navigator.currentDestination?.content?.let {
ReadingPage(
navController = navController,
articleId = it.id,
homeViewModel = homeViewModel,
readingViewModel = readingViewModel
)
}
}
}
)
}

View File

@ -1,13 +1,6 @@
package me.ash.reader.ui.page.home.flow
import android.util.Log
import android.view.HapticFeedbackConstants
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@ -17,7 +10,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -43,32 +35,27 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import coil.size.Precision
import coil.size.Scale
@ -104,6 +91,7 @@ private const val TAG = "ArticleItem"
fun ArticleItem(
modifier: Modifier = Modifier,
articleWithFeed: ArticleWithFeed,
isHighlighted: Boolean = false,
onClick: (ArticleWithFeed) -> Unit = {},
onLongClick: (() -> Unit)? = null
) {
@ -120,6 +108,7 @@ fun ArticleItem(
imgData = article.img,
isStarred = article.isStarred,
isUnread = article.isUnread,
isHighlighted = isHighlighted,
onClick = { onClick(articleWithFeed) },
onLongClick = onLongClick
)
@ -138,6 +127,7 @@ fun ArticleItem(
imgData: Any? = null,
isStarred: Boolean = false,
isUnread: Boolean = false,
isHighlighted: Boolean = false,
onClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null
) {
@ -152,12 +142,14 @@ fun ArticleItem(
modifier = modifier
.padding(horizontal = 12.dp)
.clip(Shape20)
.background(if (isHighlighted) MaterialTheme.colorScheme.primaryContainer else Color.Transparent)
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick,
)
.padding(horizontal = 12.dp, vertical = 12.dp)
.alpha(
if (isHighlighted) 1f else {
when (articleListReadIndicator) {
FlowArticleReadIndicatorPreference.AllRead -> {
if (isUnread) 1f else 0.5f
@ -167,6 +159,7 @@ fun ArticleItem(
if (isUnread || isStarred) 1f else 0.5f
}
}
}
),
) {
// Top
@ -292,10 +285,9 @@ private const val SwipeActionDelay = 300L
@Composable
fun SwipeableArticleItem(
articleWithFeed: ArticleWithFeed,
isFilterUnread: Boolean = false,
isHighlighted: Boolean = false,
articleListTonalElevation: Int = 0,
onClick: (ArticleWithFeed) -> Unit = {},
isSwipeEnabled: () -> Boolean = { false },
isMenuEnabled: Boolean = true,
onToggleStarred: (ArticleWithFeed) -> Unit = { },
onToggleRead: (ArticleWithFeed) -> Unit = { },
@ -349,8 +341,9 @@ fun SwipeableArticleItem(
) {
ArticleItem(
articleWithFeed = articleWithFeed,
isHighlighted = isHighlighted,
onClick = onClick,
onLongClick = onLongClick
onLongClick = onLongClick,
)
with(articleWithFeed.article) {
if (isMenuEnabled) {

View File

@ -16,11 +16,10 @@ import me.ash.reader.domain.model.article.ArticleWithFeed
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.ArticleList(
pagingItems: LazyPagingItems<ArticleFlowItem>,
isFilterUnread: Boolean,
isShowFeedIcon: Boolean,
isShowStickyHeader: Boolean,
articleListTonalElevation: Int,
isSwipeEnabled: () -> Boolean = { false },
readingArticleId: String?,
isMenuEnabled: Boolean = true,
onClick: (ArticleWithFeed) -> Unit = {},
onToggleStarred: (ArticleWithFeed) -> Unit = { },
@ -42,10 +41,9 @@ fun LazyListScope.ArticleList(
is ArticleFlowItem.Article -> {
SwipeableArticleItem(
articleWithFeed = item.articleWithFeed,
isFilterUnread = isFilterUnread,
isHighlighted = readingArticleId == item.articleWithFeed.article.id,
articleListTonalElevation = articleListTonalElevation,
onClick = onClick,
isSwipeEnabled = isSwipeEnabled,
isMenuEnabled = isMenuEnabled,
onToggleStarred = onToggleStarred,
onToggleRead = onToggleRead,
@ -72,10 +70,9 @@ fun LazyListScope.ArticleList(
item(key = key(item), contentType = contentType(item)) {
SwipeableArticleItem(
articleWithFeed = item.articleWithFeed,
isFilterUnread = isFilterUnread,
isHighlighted = readingArticleId == item.articleWithFeed.article.id,
articleListTonalElevation = articleListTonalElevation,
onClick = onClick,
isSwipeEnabled = isSwipeEnabled,
isMenuEnabled = isMenuEnabled,
onToggleStarred = onToggleStarred,
onToggleRead = onToggleRead,

View File

@ -65,7 +65,9 @@ import me.ash.reader.ui.page.home.HomeViewModel
)
@Composable
fun FlowPage(
navController: NavHostController,
onNavigateToFeeds: () -> Unit,
readingArticleId: String?,
onOpenArticle: (articleId: String) -> Unit,
flowViewModel: FlowViewModel = hiltViewModel(),
homeViewModel: HomeViewModel,
) {
@ -189,13 +191,7 @@ fun FlowPage(
tint = MaterialTheme.colorScheme.onSurface
) {
onSearch = false
if (navController.previousBackStackEntry == null) {
navController.navigate(RouteName.FEEDS) {
launchSingleTop = true
}
} else {
navController.popBackStack()
}
onNavigateToFeeds()
}
},
actions = {
@ -318,16 +314,13 @@ fun FlowPage(
}
ArticleList(
pagingItems = pagingItems,
isFilterUnread = filterUiState.filter == Filter.Unread,
readingArticleId = readingArticleId,
isShowFeedIcon = articleListFeedIcon.value,
isShowStickyHeader = articleListDateStickyHeader.value,
articleListTonalElevation = articleListTonalElevation.value,
isSwipeEnabled = { listState.isScrollInProgress },
onClick = {
onSearch = false
navController.navigate("${RouteName.READING}/${it.article.id}") {
launchSingleTop = true
}
onOpenArticle(it.article.id)
},
onToggleStarred = onToggleStarred,
onToggleRead = onToggleRead,

View File

@ -52,6 +52,7 @@ private const val DOWNWARD = -1
@Composable
fun ReadingPage(
navController: NavHostController,
articleId: String?,
homeViewModel: HomeViewModel,
readingViewModel: ReadingViewModel = hiltViewModel(),
) {
@ -75,7 +76,8 @@ fun ReadingPage(
val pagingItems = homeUiState.pagingData.collectAsLazyPagingItems().itemSnapshotList
LaunchedEffect(Unit) {
LaunchedEffect(articleId) {
if (articleId == null) {
navController.currentBackStackEntryFlow.collect {
it.arguments?.getString("articleId")?.let { articleId ->
if (readerState.articleId != articleId) {
@ -83,6 +85,9 @@ fun ReadingPage(
}
}
}
} else {
readingViewModel.initData(articleId)
}
}
LaunchedEffect(readerState.articleId, pagingItems.isNotEmpty()) {

View File

@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.aboutlibraries) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.kotlin.parcelize) apply false
}
tasks.register<Delete>("clean") {

View File

@ -30,6 +30,7 @@ work = "2.9.0"
composeBom = "2024.09.02"
composeCompiler = "1.5.8"
composeHtml = "1.0.2"
material3Adaptive = "1.0.0"
# Coil
coil = "2.5.0"
@ -72,6 +73,10 @@ compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "material3Adaptive" }
compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "material3Adaptive" }
compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "material3Adaptive" }
# Accompanist
accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version.ref = "accompanist" }
@ -134,3 +139,4 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibsRelease" }
room = { id = "androidx.room", version.ref = "room" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }