Optimize the reading page

This commit is contained in:
Ash 2022-05-23 20:54:06 +08:00
parent ae394da387
commit ba3620d84f
22 changed files with 525 additions and 486 deletions

View File

@ -166,9 +166,9 @@ dependencies {
// https://developer.android.com/jetpack/androidx/releases/compose-material // https://developer.android.com/jetpack/androidx/releases/compose-material
implementation "androidx.compose.material:material:$compose" implementation "androidx.compose.material:material:$compose"
implementation "androidx.compose.material:material-icons-extended:$compose" implementation "androidx.compose.material:material-icons-extended:$compose"
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
implementation "androidx.compose.ui:ui-tooling-preview:$compose" implementation "androidx.compose.ui:ui-tooling-preview:$compose"
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose" androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose"
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
// hilt // hilt
implementation "androidx.hilt:hilt-work:1.0.0" implementation "androidx.hilt:hilt-work:1.0.0"

View File

@ -1,6 +1,6 @@
package me.ash.reader.ui.component.base package me.ash.reader.ui.component.base
import androidx.compose.animation.* import RYExtensibleVisibility
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -38,11 +38,7 @@ fun AnimatedPopup(
} }
}, },
) { ) {
AnimatedVisibility( RYExtensibleVisibility(visible = visible) {
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
content() content()
} }
} }

View File

@ -1,6 +1,6 @@
package me.ash.reader.ui.component.base package me.ash.reader.ui.component.base
import androidx.compose.animation.* import RYExtensibleVisibility
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -41,11 +41,7 @@ fun DisplayText(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
AnimatedVisibility( RYExtensibleVisibility(visible = desc.isNotEmpty()) {
visible = desc.isNotEmpty(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Text( Text(
modifier = Modifier.height(16.dp), modifier = Modifier.height(16.dp),
text = desc, text = desc,

View File

@ -1,7 +1,7 @@
package me.ash.reader.ui.component.base package me.ash.reader.ui.component.base
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.material3.MaterialTheme import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -10,10 +10,7 @@ import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import coil.compose.rememberImagePainter
import androidx.compose.ui.res.painterResource
import coil.compose.LocalImageLoader
import coil.request.ImageRequest
import coil.size.Precision import coil.size.Precision
import coil.size.Scale import coil.size.Scale
import coil.size.Size import coil.size.Size
@ -33,34 +30,51 @@ fun RYAsyncImage(
@DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp, @DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp,
@DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp, @DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp,
) { ) {
coil.compose.AsyncImage( Image(
modifier = modifier, painter = rememberImagePainter(
model = ImageRequest data = data,
.Builder(LocalContext.current) builder = {
.data(data) if (placeholder != null) placeholder(placeholder)
.crossfade(true) if (error != null) error(error)
.scale(scale) crossfade(true)
.precision(precision) scale(scale)
.size(size) precision(precision)
.build(), size(size)
},
),
contentDescription = contentDescription, contentDescription = contentDescription,
contentScale = contentScale, contentScale = contentScale,
imageLoader = LocalImageLoader.current, modifier = modifier,
placeholder = placeholder?.run {
forwardingPainter(
painter = painterResource(this),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
alpha = 0.1f,
)
},
error = error?.run {
forwardingPainter(
painter = painterResource(this),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
alpha = 0.1f,
)
},
) )
// coil.compose.AsyncImage(
// modifier = modifier,
// model = ImageRequest
// .Builder(LocalContext.current)
// .data(data)
// .crossfade(true)
// .scale(scale)
// .precision(precision)
// .size(size)
// .build(),
// contentDescription = contentDescription,
// contentScale = contentScale,
// imageLoader = LocalImageLoader.current,
// placeholder = placeholder?.run {
// forwardingPainter(
// painter = painterResource(this),
// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
// alpha = 0.1f,
// )
// },
// error = error?.run {
// forwardingPainter(
// painter = painterResource(this),
// colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
// alpha = 0.1f,
// )
// },
// )
} }
// From: https://gist.github.com/colinrtwhite/c2966e0b8584b4cdf0a5b05786b20ae1 // From: https://gist.github.com/colinrtwhite/c2966e0b8584b4cdf0a5b05786b20ae1

View File

@ -0,0 +1,15 @@
import androidx.compose.animation.*
import androidx.compose.runtime.Composable
@Composable
fun RYExtensibleVisibility(
visible: Boolean,
content: @Composable AnimatedVisibilityScope.() -> Unit
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
content = content,
)
}

View File

@ -26,7 +26,7 @@ import me.ash.reader.ui.theme.palette.alwaysLight
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun SelectionChip( fun RYSelectionChip(
content: String, content: String,
selected: Boolean, selected: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

@ -27,7 +27,8 @@ import android.util.Log
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import me.ash.reader.R import me.ash.reader.R
fun LazyListScope.reader( @Suppress("FunctionName")
fun LazyListScope.Reader(
context: Context, context: Context,
link: String, link: String,
content: String, content: String,

View File

@ -1,16 +1,19 @@
package me.ash.reader.ui.ext package me.ash.reader.ui.ext
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.ln import kotlin.math.ln
@Composable
fun ColorScheme.surfaceColorAtElevation( fun ColorScheme.surfaceColorAtElevation(
elevation: Dp, elevation: Dp,
color: Color = surface, color: Color = surface,
): Color = color.atElevation(surfaceTint, elevation) ): Color = remember(this, elevation, color) { color.atElevation(surfaceTint, elevation) }
fun Color.atElevation( fun Color.atElevation(
sourceColor: Color, sourceColor: Color,

View File

@ -4,9 +4,11 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import me.ash.reader.R
import me.ash.reader.data.model.Version import me.ash.reader.data.model.Version
import me.ash.reader.data.model.toVersion import me.ash.reader.data.model.toVersion
import java.io.File import java.io.File
@ -54,3 +56,18 @@ fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
fun Context.showToastLong(message: String?) { fun Context.showToastLong(message: String?) {
showToast(message, Toast.LENGTH_LONG) showToast(message, Toast.LENGTH_LONG)
} }
fun Context.share(content: String) {
startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply {
putExtra(
Intent.EXTRA_TEXT,
content,
)
type = "text/plain"
}, getString(R.string.share)))
}
fun Context.openURL(url: String? = null) {
url?.takeIf { it.trim().isNotEmpty() }
?.let { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(it))) }
}

View File

@ -1,8 +1,7 @@
package me.ash.reader.ui.ext package me.ash.reader.ui.ext
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.remember
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import kotlin.math.abs import kotlin.math.abs
@ -28,3 +27,33 @@ fun <T : Any> LazyPagingItems<T>.rememberLazyListState(): LazyListState {
else -> androidx.compose.foundation.lazy.rememberLazyListState() else -> androidx.compose.foundation.lazy.rememberLazyListState()
} }
} }
/**
* TODO: To be improved
*
* Returns whether the LazyListState is currently in the
* downward scrolling state.
*/
@Composable
fun LazyListState.isScrollDown(): Boolean {
var isScrollDown by remember { mutableStateOf(false) }
var preItemIndex by remember { mutableStateOf(0) }
var preScrollStartOffset by remember { mutableStateOf(0) }
LaunchedEffect(this) {
snapshotFlow { isScrollInProgress }.collect {
if (isScrollInProgress) {
isScrollDown = when {
firstVisibleItemIndex > preItemIndex -> true
firstVisibleItemScrollOffset < preItemIndex -> false
else -> firstVisibleItemScrollOffset > preScrollStartOffset
}
} else {
preItemIndex = firstVisibleItemIndex
preScrollStartOffset = firstVisibleItemScrollOffset
}
}
}
return isScrollDown
}

View File

@ -52,6 +52,7 @@ import androidx.compose.ui.composed
import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.DrawResult import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -173,11 +174,15 @@ private fun CacheDrawScope.onDrawScrollbar(
return { return {
if (showScrollbar) { if (showScrollbar) {
drawRect( drawRoundRect(
color = color, color = color,
topLeft = topLeft, topLeft = topLeft,
size = size, size = size,
alpha = alpha() alpha = alpha(),
cornerRadius = CornerRadius(
x = size.width,
y = size.width,
)
) )
} }
} }
@ -217,7 +222,7 @@ private fun Modifier.drawScrollbar(
val alpha = remember { Animatable(0f) } val alpha = remember { Animatable(0f) }
LaunchedEffect(scrolled, alpha) { LaunchedEffect(scrolled, alpha) {
scrolled.collectLatest { scrolled.collectLatest {
alpha.snapTo(1f) alpha.snapTo(0.3f)
delay(ViewConfiguration.getScrollDefaultDelay().toLong()) delay(ViewConfiguration.getScrollDefaultDelay().toLong())
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec) alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
} }
@ -231,7 +236,7 @@ private fun Modifier.drawScrollbar(
// Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664 // Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664
val thickness = with(LocalDensity.current) { Thickness.toPx() } val thickness = with(LocalDensity.current) { Thickness.toPx() }
val color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) val color = MaterialTheme.colorScheme.onSurfaceVariant
Modifier Modifier
.nestedScroll(nestedScrollConnection) .nestedScroll(nestedScrollConnection)
.drawWithCache { .drawWithCache {

View File

@ -1,7 +1,7 @@
package me.ash.reader.ui.page.home.feeds package me.ash.reader.ui.page.home.feeds
import RYExtensibleVisibility
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import androidx.compose.animation.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@ -104,11 +104,7 @@ fun GroupItem(
} }
} }
Spacer(modifier = Modifier.height(22.dp)) Spacer(modifier = Modifier.height(22.dp))
AnimatedVisibility( RYExtensibleVisibility(visible = expanded) {
visible = expanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Column { Column {
feeds.forEach { feed -> feeds.forEach { feed ->
FeedItem( FeedItem(

View File

@ -34,7 +34,7 @@ import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.ui.component.base.BottomDrawer import me.ash.reader.ui.component.base.BottomDrawer
import me.ash.reader.ui.component.base.SelectionChip import me.ash.reader.ui.component.base.RYSelectionChip
import me.ash.reader.ui.component.base.Subtitle import me.ash.reader.ui.component.base.Subtitle
import me.ash.reader.ui.component.base.TextFieldDialog import me.ash.reader.ui.component.base.TextFieldDialog
import me.ash.reader.ui.ext.* import me.ash.reader.ui.ext.*
@ -162,7 +162,7 @@ private fun Preset(
crossAxisSpacing = 10.dp, crossAxisSpacing = 10.dp,
mainAxisSpacing = 10.dp, mainAxisSpacing = 10.dp,
) { ) {
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.allow_notification), content = stringResource(R.string.allow_notification),
selected = false, selected = false,
@ -178,7 +178,7 @@ private fun Preset(
) { ) {
groupOptionViewModel.showAllAllowNotificationDialog() groupOptionViewModel.showAllAllowNotificationDialog()
} }
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.parse_full_content), content = stringResource(R.string.parse_full_content),
selected = false, selected = false,
@ -194,7 +194,7 @@ private fun Preset(
) { ) {
groupOptionViewModel.showAllParseFullContentDialog() groupOptionViewModel.showAllParseFullContentDialog()
} }
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.clear_articles), content = stringResource(R.string.clear_articles),
selected = false, selected = false,
@ -202,7 +202,7 @@ private fun Preset(
groupOptionViewModel.showClearDialog() groupOptionViewModel.showClearDialog()
} }
if (group?.id != context.currentAccountId.getDefaultGroupId()) { if (group?.id != context.currentAccountId.getDefaultGroupId()) {
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.delete_group), content = stringResource(R.string.delete_group),
selected = false, selected = false,
@ -227,7 +227,7 @@ private fun FlowRowGroups(
) { ) {
groupOptionUiState.groups.forEach { groupOptionUiState.groups.forEach {
if (it.id != group?.id) { if (it.id != group?.id) {
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = it.name, content = it.name,
selected = false, selected = false,
@ -248,7 +248,7 @@ private fun LazyRowGroups(
LazyRow { LazyRow {
items(groupOptionUiState.groups) { items(groupOptionUiState.groups) {
if (it.id != group?.id) { if (it.id != group?.id) {
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = it.name, content = it.name,
selected = false, selected = false,

View File

@ -30,7 +30,7 @@ import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment import com.google.accompanist.flowlayout.MainAxisAlignment
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.Group import me.ash.reader.data.entity.Group
import me.ash.reader.ui.component.base.SelectionChip import me.ash.reader.ui.component.base.RYSelectionChip
import me.ash.reader.ui.component.base.Subtitle import me.ash.reader.ui.component.base.Subtitle
import me.ash.reader.ui.ext.roundClick import me.ash.reader.ui.ext.roundClick
import me.ash.reader.ui.theme.palette.alwaysLight import me.ash.reader.ui.theme.palette.alwaysLight
@ -127,7 +127,7 @@ private fun Preset(
crossAxisSpacing = 10.dp, crossAxisSpacing = 10.dp,
mainAxisSpacing = 10.dp, mainAxisSpacing = 10.dp,
) { ) {
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.allow_notification), content = stringResource(R.string.allow_notification),
selected = selectedAllowNotificationPreset, selected = selectedAllowNotificationPreset,
@ -144,7 +144,7 @@ private fun Preset(
) { ) {
allowNotificationPresetOnClick() allowNotificationPresetOnClick()
} }
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.parse_full_content), content = stringResource(R.string.parse_full_content),
selected = selectedParseFullContentPreset, selected = selectedParseFullContentPreset,
@ -162,14 +162,14 @@ private fun Preset(
parseFullContentPresetOnClick() parseFullContentPresetOnClick()
} }
if (showUnsubscribe) { if (showUnsubscribe) {
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.clear_articles), content = stringResource(R.string.clear_articles),
selected = false, selected = false,
) { ) {
clearArticlesOnClick() clearArticlesOnClick()
} }
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = stringResource(R.string.unsubscribe), content = stringResource(R.string.unsubscribe),
selected = false, selected = false,
@ -196,7 +196,7 @@ private fun AddToGroup(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
items(groups) { items(groups) {
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = it.name, content = it.name,
selected = it.id == selectedGroupId, selected = it.id == selectedGroupId,
@ -215,7 +215,7 @@ private fun AddToGroup(
mainAxisSpacing = 10.dp, mainAxisSpacing = 10.dp,
) { ) {
groups.forEach { groups.forEach {
SelectionChip( RYSelectionChip(
modifier = Modifier.animateContentSize(), modifier = Modifier.animateContentSize(),
content = it.name, content = it.name,
selected = it.id == selectedGroupId, selected = it.id == selectedGroupId,

View File

@ -1,7 +1,7 @@
package me.ash.reader.ui.page.home.flow package me.ash.reader.ui.page.home.flow
import RYExtensibleVisibility
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@ -19,7 +19,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -121,11 +120,7 @@ fun FlowPage(
} }
}, },
actions = { actions = {
AnimatedVisibility( RYExtensibleVisibility(visible = !filterUiState.filter.isStarred()) {
visible = !filterUiState.filter.isStarred(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
FeedbackIconButton( FeedbackIconButton(
imageVector = Icons.Rounded.DoneAll, imageVector = Icons.Rounded.DoneAll,
contentDescription = stringResource(R.string.mark_all_as_read), contentDescription = stringResource(R.string.mark_all_as_read),
@ -171,11 +166,7 @@ fun FlowPage(
) { ) {
item { item {
DisplayTextHeader(filterUiState, isSyncing, articleListFeedIcon.value) DisplayTextHeader(filterUiState, isSyncing, articleListFeedIcon.value)
AnimatedVisibility( RYExtensibleVisibility(visible = markAsRead) {
visible = markAsRead,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Spacer(modifier = Modifier.height((56 + 24 + 10).dp)) Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
} }
MarkAsReadBar( MarkAsReadBar(
@ -193,11 +184,7 @@ fun FlowPage(
markAsReadBefore = it, markAsReadBefore = it,
) )
} }
AnimatedVisibility( RYExtensibleVisibility(visible = onSearch) {
visible = onSearch,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
SearchBar( SearchBar(
value = homeUiState.searchContent, value = homeUiState.searchContent,
placeholder = when { placeholder = when {

View File

@ -0,0 +1,131 @@
package me.ash.reader.ui.page.home.reading
import RYExtensibleVisibility
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FiberManualRecord
import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.outlined.Headphones
import androidx.compose.material.icons.rounded.Article
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import me.ash.reader.R
import me.ash.reader.ui.component.base.CanBeDisabledIconButton
@Composable
fun BottomBar(
isShow: Boolean,
isUnread: Boolean,
isStarred: Boolean,
isFullContent: Boolean,
onUnread: (isUnread: Boolean) -> Unit = {},
onStarred: (isStarred: Boolean) -> Unit = {},
onFullContent: (isFullContent: Boolean) -> Unit = {},
) {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f),
contentAlignment = Alignment.BottomCenter
) {
RYExtensibleVisibility(visible = isShow) {
val view = LocalView.current
Surface(modifier = Modifier.navigationBarsPadding()) {
// TODO: Component styles await refactoring
Row(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = false,
imageVector = if (isUnread) {
Icons.Filled.FiberManualRecord
} else {
Icons.Outlined.FiberManualRecord
},
contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
tint = if (isUnread) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
onUnread(!isUnread)
}
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = false,
imageVector = if (isStarred) {
Icons.Rounded.Star
} else {
Icons.Rounded.StarOutline
},
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
tint = if (isStarred) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
onStarred(!isStarred)
}
CanBeDisabledIconButton(
disabled = true,
modifier = Modifier.size(40.dp),
imageVector = Icons.Rounded.ExpandMore,
contentDescription = "Next Article",
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
modifier = Modifier.size(36.dp),
disabled = true,
imageVector = Icons.Outlined.Headphones,
contentDescription = "Add Tag",
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
disabled = false,
modifier = Modifier.size(40.dp),
imageVector = if (isFullContent) {
Icons.Rounded.Article
} else {
Icons.Outlined.Article
},
contentDescription = stringResource(R.string.parse_full_content),
tint = if (isFullContent) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
onFullContent(!isFullContent)
}
}
}
}
}
}

View File

@ -0,0 +1,101 @@
package me.ash.reader.ui.page.home.reading
import RYExtensibleVisibility
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import me.ash.reader.ui.component.reader.Reader
import me.ash.reader.ui.ext.drawVerticalScrollbar
import java.util.*
@Composable
fun Content(
content: String,
feedName: String,
title: String,
author: String? = null,
link: String? = null,
publishedDate: Date,
listState: LazyListState,
isLoading: Boolean,
isShowToolBar: Boolean,
) {
val context = LocalContext.current
SelectionContainer {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.run {
if (isShowToolBar) {
navigationBarsPadding()
} else {
this
}
}
.drawVerticalScrollbar(listState),
state = listState,
) {
item {
// Top bar height
Spacer(modifier = Modifier.height(64.dp))
// padding
Spacer(modifier = Modifier.height(22.dp))
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
) {
DisableSelection {
Header(
feedName = feedName,
title = title,
author = author,
link = link,
publishedDate = publishedDate,
)
}
}
}
item {
Spacer(modifier = Modifier.height(22.dp))
RYExtensibleVisibility(visible = isLoading) {
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 (!isLoading) {
Reader(
context = context,
link = link ?: "",
content = content
)
}
item {
Spacer(modifier = Modifier.height(128.dp))
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
}
}

View File

@ -1,62 +1,67 @@
package me.ash.reader.ui.page.home.reading package me.ash.reader.ui.page.home.reading
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.ext.formatAsString import me.ash.reader.ui.ext.formatAsString
import me.ash.reader.ui.ext.openURL
import me.ash.reader.ui.ext.roundClick import me.ash.reader.ui.ext.roundClick
import java.util.*
@Composable @Composable
fun Header( fun Header(
articleWithFeed: ArticleWithFeed, feedName: String,
title: String,
author: String? = null,
link: String? = null,
publishedDate: Date,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val dateString = remember(publishedDate) {
publishedDate.formatAsString(context, atHourMinute = true)
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.roundClick { .roundClick {
articleWithFeed.article.link.let { context.openURL(link)
if (it.isNotEmpty()) {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(articleWithFeed.article.link))
)
}
}
} }
.padding(12.dp) .padding(12.dp)
) { ) {
Text( Text(
text = articleWithFeed.article.date.formatAsString(context, atHourMinute = true), modifier = Modifier.alpha(0.7f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), text = dateString,
color = MaterialTheme.colorScheme.outline,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = articleWithFeed.article.title, text = title,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
articleWithFeed.article.author?.let { author?.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
Text( Text(
modifier = Modifier.alpha(0.7f),
text = it, text = it,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.outline,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
) )
} }
} }
Text( Text(
text = articleWithFeed.feed.name, modifier = Modifier.alpha(0.7f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f), text = feedName,
color = MaterialTheme.colorScheme.outline,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
) )
} }

View File

@ -1,140 +0,0 @@
package me.ash.reader.ui.page.home.reading
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FiberManualRecord
import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.FiberManualRecord
import androidx.compose.material.icons.outlined.Headphones
import androidx.compose.material.icons.rounded.Article
import androidx.compose.material.icons.rounded.ExpandMore
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material.icons.rounded.StarOutline
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import me.ash.reader.R
import me.ash.reader.ui.component.base.CanBeDisabledIconButton
@Composable
fun ReadingBar(
modifier: Modifier = Modifier,
disabled: Boolean,
isUnread: Boolean,
isStarred: Boolean,
isFullContent: Boolean,
unreadOnClick: (afterIsUnread: Boolean) -> Unit = {},
starredOnClick: (afterIsStarred: Boolean) -> Unit = {},
fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {},
) {
val view = LocalView.current
var fullContent by remember { mutableStateOf(isFullContent) }
Surface(
modifier = modifier.background(MaterialTheme.colorScheme.surface),
tonalElevation = 0.dp,
) {
Box(
modifier = Modifier.height(60.dp)
) {
Box {
Divider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.zIndex(1f),
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.24f)
)
}
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
) {
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = if (isUnread) {
Icons.Filled.FiberManualRecord
} else {
Icons.Outlined.FiberManualRecord
},
contentDescription = stringResource(if (isUnread) R.string.mark_as_read else R.string.mark_as_unread),
tint = if (isUnread) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
unreadOnClick(!isUnread)
}
CanBeDisabledIconButton(
modifier = Modifier.size(40.dp),
disabled = disabled,
imageVector = if (isStarred) {
Icons.Rounded.Star
} else {
Icons.Rounded.StarOutline
},
contentDescription = stringResource(if (isStarred) R.string.mark_as_unstar else R.string.mark_as_starred),
tint = if (isStarred) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
starredOnClick(!isStarred)
}
CanBeDisabledIconButton(
disabled = true,
modifier = Modifier.size(40.dp),
imageVector = Icons.Rounded.ExpandMore,
contentDescription = "Next Article",
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
modifier = Modifier.size(36.dp),
disabled = true,
imageVector = Icons.Outlined.Headphones,
contentDescription = "Add Tag",
tint = MaterialTheme.colorScheme.outline,
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
}
CanBeDisabledIconButton(
disabled = disabled,
modifier = Modifier.size(40.dp),
imageVector = if (fullContent) {
Icons.Rounded.Article
} else {
Icons.Outlined.Article
},
contentDescription = stringResource(R.string.parse_full_content),
tint = if (fullContent) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.outline
},
) {
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
val afterIsFullContent = !fullContent
fullContent = afterIsFullContent
fullContentOnClick(afterIsFullContent)
}
}
}
}
}

View File

@ -1,36 +1,16 @@
package me.ash.reader.ui.page.home.reading package me.ash.reader.ui.page.home.reading
import android.content.Intent
import android.util.Log import android.util.Log
import androidx.compose.animation.* import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed
import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.component.base.RYScaffold import me.ash.reader.ui.component.base.RYScaffold
import me.ash.reader.ui.component.reader.reader
import me.ash.reader.ui.ext.collectAsStateValue import me.ash.reader.ui.ext.collectAsStateValue
import me.ash.reader.ui.ext.drawVerticalScrollbar import me.ash.reader.ui.ext.isScrollDown
@Composable @Composable
fun ReadingPage( fun ReadingPage(
@ -38,7 +18,8 @@ fun ReadingPage(
readingViewModel: ReadingViewModel = hiltViewModel(), readingViewModel: ReadingViewModel = hiltViewModel(),
) { ) {
val readingUiState = readingViewModel.readingUiState.collectAsStateValue() val readingUiState = readingViewModel.readingUiState.collectAsStateValue()
val isScrollDown = readingUiState.listState.isScrollDown() val isShowToolBar =
readingUiState.articleWithFeed != null && !readingUiState.listState.isScrollDown()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
navController.currentBackStackEntryFlow.collect { navController.currentBackStackEntryFlow.collect {
@ -59,46 +40,48 @@ fun ReadingPage(
RYScaffold( RYScaffold(
content = { content = {
Box(Modifier.fillMaxSize()) { Log.i("RLog", "TopBar: recomposition")
Box(
modifier = Modifier Box(modifier = Modifier.fillMaxSize()) {
.fillMaxSize() // Top Bar
.zIndex(1f),
contentAlignment = Alignment.TopCenter
) {
TopBar( TopBar(
isShow = readingUiState.articleWithFeed == null || !isScrollDown, isShow = isShowToolBar,
title = readingUiState.articleWithFeed?.article?.title, title = readingUiState.articleWithFeed?.article?.title,
link = readingUiState.articleWithFeed?.article?.link, link = readingUiState.articleWithFeed?.article?.link,
onClose = { onClose = {
navController.popBackStack() navController.popBackStack()
}, },
) )
}
// Content
if (readingUiState.articleWithFeed != null) {
Content( Content(
content = readingUiState.content ?: "", content = readingUiState.content ?: "",
articleWithFeed = readingUiState.articleWithFeed, feedName = readingUiState.articleWithFeed.feed.name,
title = readingUiState.articleWithFeed.article.title,
author = readingUiState.articleWithFeed.article.author,
link = readingUiState.articleWithFeed.article.link,
publishedDate = readingUiState.articleWithFeed.article.date,
isLoading = readingUiState.isLoading, isLoading = readingUiState.isLoading,
listState = readingUiState.listState, listState = readingUiState.listState,
isShowToolBar = readingUiState.articleWithFeed == null || !isScrollDown, isShowToolBar = isShowToolBar,
) )
Box( }
modifier = Modifier // Bottom Bar
.fillMaxSize() if (readingUiState.articleWithFeed != null) {
.zIndex(1f),
contentAlignment = Alignment.BottomCenter
) {
BottomBar( BottomBar(
isShow = readingUiState.articleWithFeed != null && !isScrollDown, isShow = isShowToolBar,
articleWithFeed = readingUiState.articleWithFeed, isUnread = readingUiState.articleWithFeed.article.isUnread,
unreadOnClick = { isStarred = readingUiState.articleWithFeed.article.isStarred,
isFullContent = readingUiState.isFullContent,
onUnread = {
readingViewModel.markUnread(it) readingViewModel.markUnread(it)
}, },
starredOnClick = { onStarred = {
readingViewModel.markStarred(it) readingViewModel.markStarred(it)
}, },
fullContentOnClick = { afterIsFullContent -> onFullContent = {
if (afterIsFullContent) readingViewModel.renderFullContent() if (it) readingViewModel.renderFullContent()
else readingViewModel.renderDescriptionContent() else readingViewModel.renderDescriptionContent()
}, },
) )
@ -107,180 +90,3 @@ fun ReadingPage(
} }
) )
} }
@Composable
fun LazyListState.isScrollDown(): Boolean {
var isScrollDown by remember { mutableStateOf(false) }
var preItemIndex by remember { mutableStateOf(0) }
var preScrollStartOffset by remember { mutableStateOf(0) }
LaunchedEffect(this) {
snapshotFlow { isScrollInProgress }.collect {
if (isScrollInProgress) {
isScrollDown = when {
firstVisibleItemIndex > preItemIndex -> true
firstVisibleItemScrollOffset < preItemIndex -> false
else -> firstVisibleItemScrollOffset > preScrollStartOffset
}
} else {
preItemIndex = firstVisibleItemIndex
preScrollStartOffset = firstVisibleItemScrollOffset
}
}
}
return isScrollDown
}
@Composable
private fun TopBar(
isShow: Boolean,
title: String? = "",
link: String? = "",
onClose: () -> Unit = {},
) {
val context = LocalContext.current
AnimatedVisibility(
visible = isShow,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
SmallTopAppBar(
modifier = Modifier.statusBarsPadding(),
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
title = {},
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.close),
tint = MaterialTheme.colorScheme.onSurface
) {
onClose()
}
},
actions = {
FeedbackIconButton(
modifier = Modifier.size(20.dp),
imageVector = Icons.Outlined.Share,
contentDescription = stringResource(R.string.share),
tint = MaterialTheme.colorScheme.onSurface,
) {
context.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply {
putExtra(
Intent.EXTRA_TEXT,
title?.takeIf { it.isNotBlank() }?.let { it + "\n" } + link
)
type = "text/plain"
}, context.getString(R.string.share)))
}
}
)
}
}
@Composable
private fun Content(
content: String,
articleWithFeed: ArticleWithFeed?,
listState: LazyListState,
isLoading: Boolean,
isShowToolBar: Boolean,
) {
if (articleWithFeed == null) return
val context = LocalContext.current
SelectionContainer {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.run {
if (isShowToolBar) {
navigationBarsPadding()
} else {
this
}
}
.drawVerticalScrollbar(listState),
state = listState,
) {
item {
Spacer(modifier = Modifier.height(64.dp))
Spacer(modifier = Modifier.height(22.dp))
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
) {
DisableSelection {
Header(articleWithFeed)
}
}
}
item {
Spacer(modifier = Modifier.height(22.dp))
AnimatedVisibility(
visible = 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 (!isLoading) {
reader(
context = context,
link = articleWithFeed.article.link,
content = content
)
}
item {
Spacer(modifier = Modifier.height(128.dp))
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
}
}
}
}
@Composable
private fun BottomBar(
isShow: Boolean,
articleWithFeed: ArticleWithFeed?,
unreadOnClick: (afterIsUnread: Boolean) -> Unit = {},
starredOnClick: (afterIsStarred: Boolean) -> Unit = {},
fullContentOnClick: (afterIsFullContent: Boolean) -> Unit = {},
) {
articleWithFeed?.let {
AnimatedVisibility(
visible = isShow,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
ReadingBar(
modifier = Modifier.navigationBarsPadding(),
disabled = false,
isUnread = articleWithFeed.article.isUnread,
isStarred = articleWithFeed.article.isStarred,
isFullContent = articleWithFeed.feed.isFullContent,
unreadOnClick = unreadOnClick,
starredOnClick = starredOnClick,
fullContentOnClick = fullContentOnClick,
)
}
}
}

View File

@ -28,7 +28,9 @@ class ReadingViewModel @Inject constructor(
showLoading() showLoading()
viewModelScope.launch { viewModelScope.launch {
_readingUiState.update { _readingUiState.update {
it.copy(articleWithFeed = rssRepository.get().findArticleById(articleId)) it.copy(
articleWithFeed = rssRepository.get().findArticleById(articleId)
)
} }
_readingUiState.value.articleWithFeed?.let { _readingUiState.value.articleWithFeed?.let {
if (it.feed.isFullContent) internalRenderFullContent() if (it.feed.isFullContent) internalRenderFullContent()
@ -43,6 +45,7 @@ class ReadingViewModel @Inject constructor(
it.copy( it.copy(
content = it.articleWithFeed?.article?.fullContent content = it.articleWithFeed?.article?.fullContent
?: it.articleWithFeed?.article?.rawDescription ?: "", ?: it.articleWithFeed?.article?.rawDescription ?: "",
isFullContent = false
) )
} }
} }
@ -53,7 +56,7 @@ class ReadingViewModel @Inject constructor(
} }
} }
suspend fun internalRenderFullContent() { private suspend fun internalRenderFullContent() {
showLoading() showLoading()
try { try {
_readingUiState.update { _readingUiState.update {
@ -61,7 +64,8 @@ class ReadingViewModel @Inject constructor(
content = rssHelper.parseFullContent( content = rssHelper.parseFullContent(
_readingUiState.value.articleWithFeed?.article?.link ?: "", _readingUiState.value.articleWithFeed?.article?.link ?: "",
_readingUiState.value.articleWithFeed?.article?.title ?: "" _readingUiState.value.articleWithFeed?.article?.title ?: ""
) ),
isFullContent = true
) )
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -133,6 +137,7 @@ class ReadingViewModel @Inject constructor(
data class ReadingUiState( data class ReadingUiState(
val articleWithFeed: ArticleWithFeed? = null, val articleWithFeed: ArticleWithFeed? = null,
val content: String? = null, val content: String? = null,
val isFullContent: Boolean = false,
val isLoading: Boolean = true, val isLoading: Boolean = true,
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
) )

View File

@ -0,0 +1,72 @@
package me.ash.reader.ui.page.home.reading
import RYExtensibleVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import me.ash.reader.R
import me.ash.reader.ui.component.base.FeedbackIconButton
import me.ash.reader.ui.ext.share
@Composable
fun TopBar(
isShow: Boolean,
title: String? = "",
link: String? = "",
onClose: () -> Unit = {},
) {
val context = LocalContext.current
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(1f),
contentAlignment = Alignment.TopCenter
) {
RYExtensibleVisibility(visible = isShow) {
SmallTopAppBar(
modifier = Modifier.statusBarsPadding(),
colors = TopAppBarDefaults.smallTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
title = {},
navigationIcon = {
FeedbackIconButton(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.close),
tint = MaterialTheme.colorScheme.onSurface
) {
onClose()
}
},
actions = {
FeedbackIconButton(
modifier = Modifier.size(20.dp),
imageVector = Icons.Outlined.Share,
contentDescription = stringResource(R.string.share),
tint = MaterialTheme.colorScheme.onSurface,
) {
context.share(title
?.takeIf { it.isNotBlank() }
?.let { it + "\n" } + link
)
}
}
)
}
}
}