Optimize the reading page
This commit is contained in:
parent
ae394da387
commit
ba3620d84f
|
@ -166,9 +166,9 @@ dependencies {
|
|||
// https://developer.android.com/jetpack/androidx/releases/compose-material
|
||||
implementation "androidx.compose.material:material:$compose"
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose"
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose"
|
||||
|
||||
// hilt
|
||||
implementation "androidx.hilt:hilt-work:1.0.0"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package me.ash.reader.ui.component.base
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import RYExtensibleVisibility
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -38,11 +38,7 @@ fun AnimatedPopup(
|
|||
}
|
||||
},
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
RYExtensibleVisibility(visible = visible) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package me.ash.reader.ui.component.base
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import RYExtensibleVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
@ -41,11 +41,7 @@ fun DisplayText(
|
|||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = desc.isNotEmpty(),
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
RYExtensibleVisibility(visible = desc.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.height(16.dp),
|
||||
text = desc,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package me.ash.reader.ui.component.base
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil.compose.LocalImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.compose.rememberImagePainter
|
||||
import coil.size.Precision
|
||||
import coil.size.Scale
|
||||
import coil.size.Size
|
||||
|
@ -33,34 +30,51 @@ fun RYAsyncImage(
|
|||
@DrawableRes placeholder: Int? = R.drawable.ic_hourglass_empty_black_24dp,
|
||||
@DrawableRes error: Int? = R.drawable.ic_broken_image_black_24dp,
|
||||
) {
|
||||
coil.compose.AsyncImage(
|
||||
modifier = modifier,
|
||||
model = ImageRequest
|
||||
.Builder(LocalContext.current)
|
||||
.data(data)
|
||||
.crossfade(true)
|
||||
.scale(scale)
|
||||
.precision(precision)
|
||||
.size(size)
|
||||
.build(),
|
||||
Image(
|
||||
painter = rememberImagePainter(
|
||||
data = data,
|
||||
builder = {
|
||||
if (placeholder != null) placeholder(placeholder)
|
||||
if (error != null) error(error)
|
||||
crossfade(true)
|
||||
scale(scale)
|
||||
precision(precision)
|
||||
size(size)
|
||||
},
|
||||
),
|
||||
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,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
// 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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -26,7 +26,7 @@ import me.ash.reader.ui.theme.palette.alwaysLight
|
|||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun SelectionChip(
|
||||
fun RYSelectionChip(
|
||||
content: String,
|
||||
selected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
|
@ -27,7 +27,8 @@ import android.util.Log
|
|||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import me.ash.reader.R
|
||||
|
||||
fun LazyListScope.reader(
|
||||
@Suppress("FunctionName")
|
||||
fun LazyListScope.Reader(
|
||||
context: Context,
|
||||
link: String,
|
||||
content: String,
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
package me.ash.reader.ui.ext
|
||||
|
||||
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.compositeOver
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.ln
|
||||
|
||||
@Composable
|
||||
fun ColorScheme.surfaceColorAtElevation(
|
||||
elevation: Dp,
|
||||
color: Color = surface,
|
||||
): Color = color.atElevation(surfaceTint, elevation)
|
||||
): Color = remember(this, elevation, color) { color.atElevation(surfaceTint, elevation) }
|
||||
|
||||
fun Color.atElevation(
|
||||
sourceColor: Color,
|
||||
|
|
|
@ -4,9 +4,11 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.data.model.Version
|
||||
import me.ash.reader.data.model.toVersion
|
||||
import java.io.File
|
||||
|
@ -54,3 +56,18 @@ fun Context.showToast(message: String?, duration: Int = Toast.LENGTH_SHORT) {
|
|||
fun Context.showToastLong(message: String?) {
|
||||
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))) }
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
package me.ash.reader.ui.ext
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import kotlin.math.abs
|
||||
|
||||
|
@ -28,3 +27,33 @@ fun <T : Any> LazyPagingItems<T>.rememberLazyListState(): LazyListState {
|
|||
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
|
||||
}
|
|
@ -52,6 +52,7 @@ import androidx.compose.ui.composed
|
|||
import androidx.compose.ui.draw.CacheDrawScope
|
||||
import androidx.compose.ui.draw.DrawResult
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -173,11 +174,15 @@ private fun CacheDrawScope.onDrawScrollbar(
|
|||
|
||||
return {
|
||||
if (showScrollbar) {
|
||||
drawRect(
|
||||
drawRoundRect(
|
||||
color = color,
|
||||
topLeft = topLeft,
|
||||
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) }
|
||||
LaunchedEffect(scrolled, alpha) {
|
||||
scrolled.collectLatest {
|
||||
alpha.snapTo(1f)
|
||||
alpha.snapTo(0.3f)
|
||||
delay(ViewConfiguration.getScrollDefaultDelay().toLong())
|
||||
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
|
||||
}
|
||||
|
@ -231,7 +236,7 @@ private fun Modifier.drawScrollbar(
|
|||
|
||||
// Calculate thickness here to workaround https://issuetracker.google.com/issues/206972664
|
||||
val thickness = with(LocalDensity.current) { Thickness.toPx() }
|
||||
val color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
val color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.drawWithCache {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package me.ash.reader.ui.page.home.feeds
|
||||
|
||||
import RYExtensibleVisibility
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
|
@ -104,11 +104,7 @@ fun GroupItem(
|
|||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
AnimatedVisibility(
|
||||
visible = expanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
RYExtensibleVisibility(visible = expanded) {
|
||||
Column {
|
||||
feeds.forEach { feed ->
|
||||
FeedItem(
|
||||
|
|
|
@ -34,7 +34,7 @@ import kotlinx.coroutines.launch
|
|||
import me.ash.reader.R
|
||||
import me.ash.reader.data.entity.Group
|
||||
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.TextFieldDialog
|
||||
import me.ash.reader.ui.ext.*
|
||||
|
@ -162,7 +162,7 @@ private fun Preset(
|
|||
crossAxisSpacing = 10.dp,
|
||||
mainAxisSpacing = 10.dp,
|
||||
) {
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = stringResource(R.string.allow_notification),
|
||||
selected = false,
|
||||
|
@ -178,7 +178,7 @@ private fun Preset(
|
|||
) {
|
||||
groupOptionViewModel.showAllAllowNotificationDialog()
|
||||
}
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = stringResource(R.string.parse_full_content),
|
||||
selected = false,
|
||||
|
@ -194,7 +194,7 @@ private fun Preset(
|
|||
) {
|
||||
groupOptionViewModel.showAllParseFullContentDialog()
|
||||
}
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = stringResource(R.string.clear_articles),
|
||||
selected = false,
|
||||
|
@ -202,7 +202,7 @@ private fun Preset(
|
|||
groupOptionViewModel.showClearDialog()
|
||||
}
|
||||
if (group?.id != context.currentAccountId.getDefaultGroupId()) {
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = stringResource(R.string.delete_group),
|
||||
selected = false,
|
||||
|
@ -227,7 +227,7 @@ private fun FlowRowGroups(
|
|||
) {
|
||||
groupOptionUiState.groups.forEach {
|
||||
if (it.id != group?.id) {
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = it.name,
|
||||
selected = false,
|
||||
|
@ -248,7 +248,7 @@ private fun LazyRowGroups(
|
|||
LazyRow {
|
||||
items(groupOptionUiState.groups) {
|
||||
if (it.id != group?.id) {
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = it.name,
|
||||
selected = false,
|
||||
|
|
|
@ -30,7 +30,7 @@ import com.google.accompanist.flowlayout.FlowRow
|
|||
import com.google.accompanist.flowlayout.MainAxisAlignment
|
||||
import me.ash.reader.R
|
||||
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.ext.roundClick
|
||||
import me.ash.reader.ui.theme.palette.alwaysLight
|
||||
|
@ -127,7 +127,7 @@ private fun Preset(
|
|||
crossAxisSpacing = 10.dp,
|
||||
mainAxisSpacing = 10.dp,
|
||||
) {
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = stringResource(R.string.allow_notification),
|
||||
selected = selectedAllowNotificationPreset,
|
||||
|
@ -144,7 +144,7 @@ private fun Preset(
|
|||
) {
|
||||
allowNotificationPresetOnClick()
|
||||
}
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = stringResource(R.string.parse_full_content),
|
||||
selected = selectedParseFullContentPreset,
|
||||
|
@ -162,14 +162,14 @@ private fun Preset(
|
|||
parseFullContentPresetOnClick()
|
||||
}
|
||||
if (showUnsubscribe) {
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = stringResource(R.string.clear_articles),
|
||||
selected = false,
|
||||
) {
|
||||
clearArticlesOnClick()
|
||||
}
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = stringResource(R.string.unsubscribe),
|
||||
selected = false,
|
||||
|
@ -196,7 +196,7 @@ private fun AddToGroup(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
items(groups) {
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = it.name,
|
||||
selected = it.id == selectedGroupId,
|
||||
|
@ -215,7 +215,7 @@ private fun AddToGroup(
|
|||
mainAxisSpacing = 10.dp,
|
||||
) {
|
||||
groups.forEach {
|
||||
SelectionChip(
|
||||
RYSelectionChip(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
content = it.name,
|
||||
selected = it.id == selectedGroupId,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package me.ash.reader.ui.page.home.flow
|
||||
|
||||
import RYExtensibleVisibility
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
|
@ -19,7 +19,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -121,11 +120,7 @@ fun FlowPage(
|
|||
}
|
||||
},
|
||||
actions = {
|
||||
AnimatedVisibility(
|
||||
visible = !filterUiState.filter.isStarred(),
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
RYExtensibleVisibility(visible = !filterUiState.filter.isStarred()) {
|
||||
FeedbackIconButton(
|
||||
imageVector = Icons.Rounded.DoneAll,
|
||||
contentDescription = stringResource(R.string.mark_all_as_read),
|
||||
|
@ -171,11 +166,7 @@ fun FlowPage(
|
|||
) {
|
||||
item {
|
||||
DisplayTextHeader(filterUiState, isSyncing, articleListFeedIcon.value)
|
||||
AnimatedVisibility(
|
||||
visible = markAsRead,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
RYExtensibleVisibility(visible = markAsRead) {
|
||||
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
|
||||
}
|
||||
MarkAsReadBar(
|
||||
|
@ -193,11 +184,7 @@ fun FlowPage(
|
|||
markAsReadBefore = it,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = onSearch,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
RYExtensibleVisibility(visible = onSearch) {
|
||||
SearchBar(
|
||||
value = homeUiState.searchContent,
|
||||
placeholder = when {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
101
app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt
Normal file
101
app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,62 +1,67 @@
|
|||
package me.ash.reader.ui.page.home.reading
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.openURL
|
||||
import me.ash.reader.ui.ext.roundClick
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun Header(
|
||||
articleWithFeed: ArticleWithFeed,
|
||||
feedName: String,
|
||||
title: String,
|
||||
author: String? = null,
|
||||
link: String? = null,
|
||||
publishedDate: Date,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val dateString = remember(publishedDate) {
|
||||
publishedDate.formatAsString(context, atHourMinute = true)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.roundClick {
|
||||
articleWithFeed.article.link.let {
|
||||
if (it.isNotEmpty()) {
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(articleWithFeed.article.link))
|
||||
)
|
||||
}
|
||||
}
|
||||
context.openURL(link)
|
||||
}
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = articleWithFeed.article.date.formatAsString(context, atHourMinute = true),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||
modifier = Modifier.alpha(0.7f),
|
||||
text = dateString,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = articleWithFeed.article.title,
|
||||
text = title,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
articleWithFeed.article.author?.let {
|
||||
author?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.alpha(0.7f),
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = articleWithFeed.feed.name,
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f),
|
||||
modifier = Modifier.alpha(0.7f),
|
||||
text = feedName,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +1,16 @@
|
|||
package me.ash.reader.ui.page.home.reading
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.*
|
||||
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.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.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.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.reader.reader
|
||||
import me.ash.reader.ui.ext.collectAsStateValue
|
||||
import me.ash.reader.ui.ext.drawVerticalScrollbar
|
||||
import me.ash.reader.ui.ext.isScrollDown
|
||||
|
||||
@Composable
|
||||
fun ReadingPage(
|
||||
|
@ -38,7 +18,8 @@ fun ReadingPage(
|
|||
readingViewModel: ReadingViewModel = hiltViewModel(),
|
||||
) {
|
||||
val readingUiState = readingViewModel.readingUiState.collectAsStateValue()
|
||||
val isScrollDown = readingUiState.listState.isScrollDown()
|
||||
val isShowToolBar =
|
||||
readingUiState.articleWithFeed != null && !readingUiState.listState.isScrollDown()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
navController.currentBackStackEntryFlow.collect {
|
||||
|
@ -59,46 +40,48 @@ fun ReadingPage(
|
|||
|
||||
RYScaffold(
|
||||
content = {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f),
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
TopBar(
|
||||
isShow = readingUiState.articleWithFeed == null || !isScrollDown,
|
||||
title = readingUiState.articleWithFeed?.article?.title,
|
||||
link = readingUiState.articleWithFeed?.article?.link,
|
||||
onClose = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
Log.i("RLog", "TopBar: recomposition")
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// Top Bar
|
||||
TopBar(
|
||||
isShow = isShowToolBar,
|
||||
title = readingUiState.articleWithFeed?.article?.title,
|
||||
link = readingUiState.articleWithFeed?.article?.link,
|
||||
onClose = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
)
|
||||
|
||||
// Content
|
||||
if (readingUiState.articleWithFeed != null) {
|
||||
Content(
|
||||
content = readingUiState.content ?: "",
|
||||
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,
|
||||
listState = readingUiState.listState,
|
||||
isShowToolBar = isShowToolBar,
|
||||
)
|
||||
}
|
||||
Content(
|
||||
content = readingUiState.content ?: "",
|
||||
articleWithFeed = readingUiState.articleWithFeed,
|
||||
isLoading = readingUiState.isLoading,
|
||||
listState = readingUiState.listState,
|
||||
isShowToolBar = readingUiState.articleWithFeed == null || !isScrollDown,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
// Bottom Bar
|
||||
if (readingUiState.articleWithFeed != null) {
|
||||
BottomBar(
|
||||
isShow = readingUiState.articleWithFeed != null && !isScrollDown,
|
||||
articleWithFeed = readingUiState.articleWithFeed,
|
||||
unreadOnClick = {
|
||||
isShow = isShowToolBar,
|
||||
isUnread = readingUiState.articleWithFeed.article.isUnread,
|
||||
isStarred = readingUiState.articleWithFeed.article.isStarred,
|
||||
isFullContent = readingUiState.isFullContent,
|
||||
onUnread = {
|
||||
readingViewModel.markUnread(it)
|
||||
},
|
||||
starredOnClick = {
|
||||
onStarred = {
|
||||
readingViewModel.markStarred(it)
|
||||
},
|
||||
fullContentOnClick = { afterIsFullContent ->
|
||||
if (afterIsFullContent) readingViewModel.renderFullContent()
|
||||
onFullContent = {
|
||||
if (it) readingViewModel.renderFullContent()
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,7 +28,9 @@ class ReadingViewModel @Inject constructor(
|
|||
showLoading()
|
||||
viewModelScope.launch {
|
||||
_readingUiState.update {
|
||||
it.copy(articleWithFeed = rssRepository.get().findArticleById(articleId))
|
||||
it.copy(
|
||||
articleWithFeed = rssRepository.get().findArticleById(articleId)
|
||||
)
|
||||
}
|
||||
_readingUiState.value.articleWithFeed?.let {
|
||||
if (it.feed.isFullContent) internalRenderFullContent()
|
||||
|
@ -43,6 +45,7 @@ class ReadingViewModel @Inject constructor(
|
|||
it.copy(
|
||||
content = it.articleWithFeed?.article?.fullContent
|
||||
?: it.articleWithFeed?.article?.rawDescription ?: "",
|
||||
isFullContent = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +56,7 @@ class ReadingViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun internalRenderFullContent() {
|
||||
private suspend fun internalRenderFullContent() {
|
||||
showLoading()
|
||||
try {
|
||||
_readingUiState.update {
|
||||
|
@ -61,7 +64,8 @@ class ReadingViewModel @Inject constructor(
|
|||
content = rssHelper.parseFullContent(
|
||||
_readingUiState.value.articleWithFeed?.article?.link ?: "",
|
||||
_readingUiState.value.articleWithFeed?.article?.title ?: ""
|
||||
)
|
||||
),
|
||||
isFullContent = true
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -133,6 +137,7 @@ class ReadingViewModel @Inject constructor(
|
|||
data class ReadingUiState(
|
||||
val articleWithFeed: ArticleWithFeed? = null,
|
||||
val content: String? = null,
|
||||
val isFullContent: Boolean = false,
|
||||
val isLoading: Boolean = true,
|
||||
val listState: LazyListState = LazyListState(),
|
||||
)
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user