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.aboutlibraries)
alias(libs.plugins.room) alias(libs.plugins.room)
alias(libs.plugins.hilt) alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.parcelize)
} }
fun fetchGitCommitHash(): String { fun fetchGitCommitHash(): String {
@ -131,6 +132,9 @@ dependencies {
implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.ui.tooling.preview)
androidTestImplementation(libs.compose.ui.test.junit4) androidTestImplementation(libs.compose.ui.test.junit4)
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.material3.adaptive)
implementation(libs.compose.material3.adaptive.layout)
implementation(libs.compose.material3.adaptive.navigation)
// Accompanist // Accompanist
implementation(libs.accompanist.swiperefresh) 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.initialPage
import me.ash.reader.ui.ext.isFirstLaunch import me.ash.reader.ui.ext.isFirstLaunch
import me.ash.reader.ui.page.home.HomeViewModel 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.FeedsPage
import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel import me.ash.reader.ui.page.home.feeds.subscribe.SubscribeViewModel
import me.ash.reader.ui.page.home.flow.FlowPage import me.ash.reader.ui.page.home.flow.FlowPage
@ -164,13 +165,37 @@ fun HomeEntry(
) )
} }
animatedComposable(route = RouteName.FLOW) { animatedComposable(route = RouteName.FLOW) {
FlowPage(
ArticleListReaderPage(
modifier = Modifier,
navController = navController, 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}") { animatedComposable(route = "${RouteName.READING}/{articleId}") {
ReadingPage(navController = navController, homeViewModel = homeViewModel) val articleId = it.arguments?.getString("articleId")
ReadingPage(
navController = navController,
articleId = articleId,
homeViewModel = homeViewModel
)
} }
// Settings // 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 package me.ash.reader.ui.page.home.flow
import android.util.Log
import android.view.HapticFeedbackConstants 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.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable 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.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -43,32 +35,27 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset 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.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.size.Precision import coil.size.Precision
import coil.size.Scale import coil.size.Scale
@ -104,6 +91,7 @@ private const val TAG = "ArticleItem"
fun ArticleItem( fun ArticleItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
articleWithFeed: ArticleWithFeed, articleWithFeed: ArticleWithFeed,
isHighlighted: Boolean = false,
onClick: (ArticleWithFeed) -> Unit = {}, onClick: (ArticleWithFeed) -> Unit = {},
onLongClick: (() -> Unit)? = null onLongClick: (() -> Unit)? = null
) { ) {
@ -120,6 +108,7 @@ fun ArticleItem(
imgData = article.img, imgData = article.img,
isStarred = article.isStarred, isStarred = article.isStarred,
isUnread = article.isUnread, isUnread = article.isUnread,
isHighlighted = isHighlighted,
onClick = { onClick(articleWithFeed) }, onClick = { onClick(articleWithFeed) },
onLongClick = onLongClick onLongClick = onLongClick
) )
@ -138,6 +127,7 @@ fun ArticleItem(
imgData: Any? = null, imgData: Any? = null,
isStarred: Boolean = false, isStarred: Boolean = false,
isUnread: Boolean = false, isUnread: Boolean = false,
isHighlighted: Boolean = false,
onClick: () -> Unit = {}, onClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null onLongClick: (() -> Unit)? = null
) { ) {
@ -152,19 +142,22 @@ fun ArticleItem(
modifier = modifier modifier = modifier
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.clip(Shape20) .clip(Shape20)
.background(if (isHighlighted) MaterialTheme.colorScheme.primaryContainer else Color.Transparent)
.combinedClickable( .combinedClickable(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
) )
.padding(horizontal = 12.dp, vertical = 12.dp) .padding(horizontal = 12.dp, vertical = 12.dp)
.alpha( .alpha(
when (articleListReadIndicator) { if (isHighlighted) 1f else {
FlowArticleReadIndicatorPreference.AllRead -> { when (articleListReadIndicator) {
if (isUnread) 1f else 0.5f FlowArticleReadIndicatorPreference.AllRead -> {
} if (isUnread) 1f else 0.5f
}
FlowArticleReadIndicatorPreference.ExcludingStarred -> { FlowArticleReadIndicatorPreference.ExcludingStarred -> {
if (isUnread || isStarred) 1f else 0.5f if (isUnread || isStarred) 1f else 0.5f
}
} }
} }
), ),
@ -292,10 +285,9 @@ private const val SwipeActionDelay = 300L
@Composable @Composable
fun SwipeableArticleItem( fun SwipeableArticleItem(
articleWithFeed: ArticleWithFeed, articleWithFeed: ArticleWithFeed,
isFilterUnread: Boolean = false, isHighlighted: Boolean = false,
articleListTonalElevation: Int = 0, articleListTonalElevation: Int = 0,
onClick: (ArticleWithFeed) -> Unit = {}, onClick: (ArticleWithFeed) -> Unit = {},
isSwipeEnabled: () -> Boolean = { false },
isMenuEnabled: Boolean = true, isMenuEnabled: Boolean = true,
onToggleStarred: (ArticleWithFeed) -> Unit = { }, onToggleStarred: (ArticleWithFeed) -> Unit = { },
onToggleRead: (ArticleWithFeed) -> Unit = { }, onToggleRead: (ArticleWithFeed) -> Unit = { },
@ -349,8 +341,9 @@ fun SwipeableArticleItem(
) { ) {
ArticleItem( ArticleItem(
articleWithFeed = articleWithFeed, articleWithFeed = articleWithFeed,
isHighlighted = isHighlighted,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick onLongClick = onLongClick,
) )
with(articleWithFeed.article) { with(articleWithFeed.article) {
if (isMenuEnabled) { if (isMenuEnabled) {

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ work = "2.9.0"
composeBom = "2024.09.02" composeBom = "2024.09.02"
composeCompiler = "1.5.8" composeCompiler = "1.5.8"
composeHtml = "1.0.2" composeHtml = "1.0.2"
material3Adaptive = "1.0.0"
# Coil # Coil
coil = "2.5.0" 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-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" } 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
accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version.ref = "accompanist" } accompanist-swiperefresh = { group = "com.google.accompanist", name = "accompanist-swiperefresh", version.ref = "accompanist" }
@ -133,4 +138,5 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibsRelease" } aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibsRelease" }
room = { id = "androidx.room", version.ref = "room" } room = { id = "androidx.room", version.ref = "room" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }