mirror of https://github.com/Ashinch/ReadYou.git
Add a scrollbar to the article screen (#63)
Co-authored-by: Matt Vaughn <matt.vaughn@willowtreeapps.com>
This commit is contained in:
parent
b292535ab6
commit
b7813d45f4
|
@ -149,6 +149,7 @@ dependencies {
|
|||
implementation "androidx.compose.animation:animation-graphics:$compose"
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-ui
|
||||
implementation "androidx.compose.ui:ui:$compose"
|
||||
implementation "androidx.compose.ui:ui-util:$compose"
|
||||
// https://developer.android.com/jetpack/androidx/releases/compose-material
|
||||
implementation "androidx.compose.material:material:$compose"
|
||||
implementation "androidx.compose.material:material-icons-extended:$compose"
|
||||
|
|
|
@ -0,0 +1,320 @@
|
|||
package me.ash.reader.ui.ext
|
||||
|
||||
// From gist: https://gist.github.com/mxalbert1996/33a360fcab2105a31e5355af98216f5a/
|
||||
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2022 Albert Chang
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
import android.view.ViewConfiguration
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastSumBy
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
fun Modifier.drawHorizontalScrollbar(
|
||||
state: ScrollState,
|
||||
reverseScrolling: Boolean = false
|
||||
): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling)
|
||||
|
||||
fun Modifier.drawVerticalScrollbar(
|
||||
state: ScrollState,
|
||||
reverseScrolling: Boolean = false
|
||||
): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling)
|
||||
|
||||
fun Modifier.drawHorizontalScrollbar(
|
||||
state: LazyListState,
|
||||
reverseScrolling: Boolean = false
|
||||
): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling)
|
||||
|
||||
fun Modifier.drawVerticalScrollbar(
|
||||
state: LazyListState,
|
||||
reverseScrolling: Boolean = false
|
||||
): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling)
|
||||
|
||||
private fun Modifier.drawScrollbar(
|
||||
state: ScrollState,
|
||||
orientation: Orientation,
|
||||
reverseScrolling: Boolean
|
||||
): Modifier = drawScrollbar(
|
||||
orientation, reverseScrolling
|
||||
) { reverseDirection, atEnd, thickness, color, alpha ->
|
||||
val showScrollbar = state.maxValue > 0
|
||||
val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height
|
||||
val totalSize = canvasSize + state.maxValue
|
||||
val thumbSize = canvasSize / totalSize * canvasSize
|
||||
val startOffset = state.value / totalSize * canvasSize
|
||||
val drawScrollbar = onDrawScrollbar(
|
||||
orientation, reverseDirection, atEnd, showScrollbar,
|
||||
thickness, color, alpha, thumbSize, startOffset
|
||||
)
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
drawScrollbar()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.drawScrollbar(
|
||||
state: LazyListState,
|
||||
orientation: Orientation,
|
||||
reverseScrolling: Boolean
|
||||
): Modifier = drawScrollbar(
|
||||
orientation, reverseScrolling
|
||||
) { reverseDirection, atEnd, thickness, color, alpha ->
|
||||
val layoutInfo = state.layoutInfo
|
||||
val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
||||
val items = layoutInfo.visibleItemsInfo
|
||||
val itemsSize = items.fastSumBy { it.size }
|
||||
val showScrollbar = items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize
|
||||
val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size
|
||||
val totalSize = estimatedItemSize * layoutInfo.totalItemsCount
|
||||
val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height
|
||||
val thumbSize = viewportSize / totalSize * canvasSize
|
||||
val startOffset = if (items.isEmpty()) 0f else items
|
||||
.first()
|
||||
.run {
|
||||
(estimatedItemSize * index - offset) / totalSize * canvasSize
|
||||
}
|
||||
val drawScrollbar = onDrawScrollbar(
|
||||
orientation, reverseDirection, atEnd, showScrollbar,
|
||||
thickness, color, alpha, thumbSize, startOffset
|
||||
)
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
drawScrollbar()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CacheDrawScope.onDrawScrollbar(
|
||||
orientation: Orientation,
|
||||
reverseDirection: Boolean,
|
||||
atEnd: Boolean,
|
||||
showScrollbar: Boolean,
|
||||
thickness: Float,
|
||||
color: Color,
|
||||
alpha: () -> Float,
|
||||
thumbSize: Float,
|
||||
startOffset: Float
|
||||
): DrawScope.() -> Unit {
|
||||
val topLeft = if (orientation == Orientation.Horizontal) {
|
||||
Offset(
|
||||
if (reverseDirection) size.width - startOffset - thumbSize else startOffset,
|
||||
if (atEnd) size.height - thickness else 0f
|
||||
)
|
||||
} else {
|
||||
Offset(
|
||||
if (atEnd) size.width - thickness else 0f,
|
||||
if (reverseDirection) size.height - startOffset - thumbSize else startOffset
|
||||
)
|
||||
}
|
||||
val size = if (orientation == Orientation.Horizontal) {
|
||||
Size(thumbSize, thickness)
|
||||
} else {
|
||||
Size(thickness, thumbSize)
|
||||
}
|
||||
|
||||
return {
|
||||
if (showScrollbar) {
|
||||
drawRect(
|
||||
color = color,
|
||||
topLeft = topLeft,
|
||||
size = size,
|
||||
alpha = alpha()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.drawScrollbar(
|
||||
orientation: Orientation,
|
||||
reverseScrolling: Boolean,
|
||||
onBuildDrawCache: CacheDrawScope.(
|
||||
reverseDirection: Boolean,
|
||||
atEnd: Boolean,
|
||||
thickness: Float,
|
||||
color: Color,
|
||||
alpha: () -> Float
|
||||
) -> DrawResult
|
||||
): Modifier = composed {
|
||||
val scrolled = remember {
|
||||
MutableSharedFlow<Unit>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
}
|
||||
val nestedScrollConnection = remember(orientation, scrolled) {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
val delta = if (orientation == Orientation.Horizontal) consumed.x else consumed.y
|
||||
if (delta != 0f) scrolled.tryEmit(Unit)
|
||||
return Offset.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val alpha = remember { Animatable(0f) }
|
||||
LaunchedEffect(scrolled, alpha) {
|
||||
scrolled.collectLatest {
|
||||
alpha.snapTo(1f)
|
||||
delay(ViewConfiguration.getScrollDefaultDelay().toLong())
|
||||
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
|
||||
}
|
||||
}
|
||||
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
val reverseDirection = if (orientation == Orientation.Horizontal) {
|
||||
if (isLtr) reverseScrolling else !reverseScrolling
|
||||
} else reverseScrolling
|
||||
val atEnd = if (orientation == Orientation.Vertical) isLtr else true
|
||||
|
||||
// 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)
|
||||
Modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.drawWithCache {
|
||||
onBuildDrawCache(reverseDirection, atEnd, thickness, color, alpha::value)
|
||||
}
|
||||
}
|
||||
|
||||
private val Thickness = 4.dp
|
||||
private val FadeOutAnimationSpec =
|
||||
tween<Float>(durationMillis = ViewConfiguration.getScrollBarFadeDuration())
|
||||
|
||||
@Preview(widthDp = 400, heightDp = 400, showBackground = true)
|
||||
@Composable
|
||||
fun ScrollbarPreview() {
|
||||
val state = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.drawVerticalScrollbar(state)
|
||||
.verticalScroll(state),
|
||||
) {
|
||||
repeat(50) {
|
||||
Text(
|
||||
text = "Item ${it + 1}",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 400, heightDp = 400, showBackground = true)
|
||||
@Composable
|
||||
fun LazyListScrollbarPreview() {
|
||||
val state = rememberLazyListState()
|
||||
LazyColumn(
|
||||
modifier = Modifier.drawVerticalScrollbar(state),
|
||||
state = state
|
||||
) {
|
||||
items(50) {
|
||||
Text(
|
||||
text = "Item ${it + 1}",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 400, showBackground = true)
|
||||
@Composable
|
||||
fun HorizontalScrollbarPreview() {
|
||||
val state = rememberScrollState()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.drawHorizontalScrollbar(state)
|
||||
.horizontalScroll(state)
|
||||
) {
|
||||
repeat(50) {
|
||||
Text(
|
||||
text = (it + 1).toString(),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp, vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 400, showBackground = true)
|
||||
@Composable
|
||||
fun LazyListHorizontalScrollbarPreview() {
|
||||
val state = rememberLazyListState()
|
||||
LazyRow(
|
||||
modifier = Modifier.drawHorizontalScrollbar(state),
|
||||
state = state
|
||||
) {
|
||||
items(50) {
|
||||
Text(
|
||||
text = (it + 1).toString(),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp, vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,10 +2,10 @@ package me.ash.reader.ui.page.home.read
|
|||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Headphones
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
|
@ -25,6 +25,7 @@ import me.ash.reader.data.entity.ArticleWithFeed
|
|||
import me.ash.reader.ui.component.FeedbackIconButton
|
||||
import me.ash.reader.ui.component.WebView
|
||||
import me.ash.reader.ui.ext.collectAsStateValue
|
||||
import me.ash.reader.ui.ext.drawVerticalScrollbar
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
@ -43,21 +44,14 @@ fun ReadPage(
|
|||
}
|
||||
}
|
||||
|
||||
if (viewState.listState.isScrollInProgress) {
|
||||
if (viewState.scrollState.isScrollInProgress) {
|
||||
LaunchedEffect(Unit) {
|
||||
Log.i("RLog", "scroll: start")
|
||||
}
|
||||
|
||||
val preItemIndex by remember { mutableStateOf(viewState.listState.firstVisibleItemIndex) }
|
||||
val preScrollStartOffset by remember { mutableStateOf(viewState.listState.firstVisibleItemScrollOffset) }
|
||||
val currentItemIndex = viewState.listState.firstVisibleItemIndex
|
||||
val currentScrollStartOffset = viewState.listState.firstVisibleItemScrollOffset
|
||||
|
||||
isScrollDown = when {
|
||||
currentItemIndex > preItemIndex -> true
|
||||
currentItemIndex < preItemIndex -> false
|
||||
else -> currentScrollStartOffset > preScrollStartOffset
|
||||
}
|
||||
val preScrollOffset by remember { mutableStateOf(viewState.scrollState.value) }
|
||||
val currentOffset = viewState.scrollState.value
|
||||
isScrollDown = currentOffset > preScrollOffset
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
|
@ -98,7 +92,7 @@ fun ReadPage(
|
|||
content = viewState.content ?: "",
|
||||
articleWithFeed = viewState.articleWithFeed,
|
||||
viewState = viewState,
|
||||
LazyListState = viewState.listState,
|
||||
scrollState = viewState.scrollState,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
@ -182,10 +176,14 @@ private fun Content(
|
|||
content: String,
|
||||
articleWithFeed: ArticleWithFeed?,
|
||||
viewState: ReadViewState,
|
||||
LazyListState: LazyListState = rememberLazyListState(),
|
||||
scrollState: ScrollState = rememberScrollState(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.drawVerticalScrollbar(scrollState)
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
if (articleWithFeed == null) {
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
|
@ -196,54 +194,44 @@ private fun Content(
|
|||
// url = "https://assets8.lottiefiles.com/packages/lf20_jm7mv1ib.json",
|
||||
// )
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = LazyListState,
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
Header(articleWithFeed)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
AnimatedVisibility(
|
||||
visible = viewState.isLoading,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Header(articleWithFeed)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
AnimatedVisibility(
|
||||
visible = viewState.isLoading,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(30.dp),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
}
|
||||
if (!viewState.isLoading) {
|
||||
WebView(
|
||||
content = content
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
if (!viewState.isLoading) {
|
||||
WebView(
|
||||
content = content
|
||||
)
|
||||
Spacer(modifier = Modifier.height(50.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
Spacer(modifier = Modifier.height(64.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package me.ash.reader.ui.page.home.read
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -31,7 +31,6 @@ class ReadViewModel @Inject constructor(
|
|||
is ReadViewAction.RenderFullContent -> renderFullContent()
|
||||
is ReadViewAction.MarkUnread -> markUnread(action.isUnread)
|
||||
is ReadViewAction.MarkStarred -> markStarred(action.isStarred)
|
||||
is ReadViewAction.ScrollToItem -> scrollToItem(action.index)
|
||||
is ReadViewAction.ClearArticle -> clearArticle()
|
||||
is ReadViewAction.ChangeLoading -> changeLoading(action.isLoading)
|
||||
}
|
||||
|
@ -130,12 +129,6 @@ class ReadViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun scrollToItem(index: Int) {
|
||||
viewModelScope.launch {
|
||||
_viewState.value.listState.scrollToItem(index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearArticle() {
|
||||
_viewState.update {
|
||||
it.copy(articleWithFeed = null)
|
||||
|
@ -153,7 +146,7 @@ data class ReadViewState(
|
|||
val articleWithFeed: ArticleWithFeed? = null,
|
||||
val content: String? = null,
|
||||
val isLoading: Boolean = true,
|
||||
val listState: LazyListState = LazyListState(),
|
||||
val scrollState: ScrollState = ScrollState(0),
|
||||
)
|
||||
|
||||
sealed class ReadViewAction {
|
||||
|
@ -173,10 +166,6 @@ sealed class ReadViewAction {
|
|||
val isStarred: Boolean,
|
||||
) : ReadViewAction()
|
||||
|
||||
data class ScrollToItem(
|
||||
val index: Int
|
||||
) : ReadViewAction()
|
||||
|
||||
object ClearArticle : ReadViewAction()
|
||||
|
||||
data class ChangeLoading(
|
||||
|
|
Loading…
Reference in New Issue