feat: multiple checkpoints
This commit is contained in:
parent
57afade51a
commit
754a0d7cf3
9 changed files with 273 additions and 191 deletions
|
|
@ -9,6 +9,7 @@ package ua.acclorite.book_story.presentation.reader
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.domain.model.reader.ReaderText.Chapter
|
import ua.acclorite.book_story.domain.model.reader.ReaderText.Chapter
|
||||||
|
import ua.acclorite.book_story.presentation.reader.model.Checkpoint
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed class ReaderEvent {
|
sealed class ReaderEvent {
|
||||||
|
|
@ -39,7 +40,9 @@ sealed class ReaderEvent {
|
||||||
val progress: Float
|
val progress: Float
|
||||||
) : ReaderEvent()
|
) : ReaderEvent()
|
||||||
|
|
||||||
data object OnRestoreCheckpoint : ReaderEvent()
|
data class OnRestoreCheckpoint(
|
||||||
|
val checkpoint: Checkpoint
|
||||||
|
) : ReaderEvent()
|
||||||
|
|
||||||
data class OnLeave(
|
data class OnLeave(
|
||||||
val activity: ComponentActivity,
|
val activity: ComponentActivity,
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ class ReaderModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnMenuVisibility -> {
|
is ReaderEvent.OnMenuVisibility -> {
|
||||||
launch {
|
launch(Dispatchers.Default) {
|
||||||
if (_state.value.lockMenu) return@launch
|
if (_state.value.lockMenu) return@launch
|
||||||
|
|
||||||
yield()
|
yield()
|
||||||
|
|
@ -152,21 +152,31 @@ class ReaderModel @Inject constructor(
|
||||||
show = event.show || !event.fullscreenMode,
|
show = event.show || !event.fullscreenMode,
|
||||||
activity = event.activity
|
activity = event.activity
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val checkpoints = _state.value.checkpoints.toMutableList()
|
||||||
|
if (event.saveCheckpoint && event.show) {
|
||||||
|
checkpoints.removeIf {
|
||||||
|
it.index == _state.value.listState.firstVisibleItemIndex
|
||||||
|
}
|
||||||
|
checkpoints.add(
|
||||||
|
Checkpoint(
|
||||||
|
_state.value.listState.firstVisibleItemIndex,
|
||||||
|
_state.value.listState.firstVisibleItemScrollOffset
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
showMenu = event.show,
|
showMenu = event.show,
|
||||||
checkpoint = _state.value.listState.run {
|
checkpoints = checkpoints
|
||||||
if (!event.show || !event.saveCheckpoint) return@run it.checkpoint
|
|
||||||
|
|
||||||
Checkpoint(firstVisibleItemIndex, firstVisibleItemScrollOffset)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnChangeProgress -> {
|
is ReaderEvent.OnChangeProgress -> {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.Default) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
book = it.book.copy(
|
book = it.book.copy(
|
||||||
|
|
@ -185,7 +195,7 @@ class ReaderModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnScrollToChapter -> {
|
is ReaderEvent.OnScrollToChapter -> {
|
||||||
launch {
|
launch(Dispatchers.Default) {
|
||||||
_state.value.apply {
|
_state.value.apply {
|
||||||
val chapterIndex = text.indexOf(event.chapter).takeIf { it != -1 }
|
val chapterIndex = text.indexOf(event.chapter).takeIf { it != -1 }
|
||||||
if (chapterIndex == null) {
|
if (chapterIndex == null) {
|
||||||
|
|
@ -207,40 +217,52 @@ class ReaderModel @Inject constructor(
|
||||||
|
|
||||||
is ReaderEvent.OnScroll -> {
|
is ReaderEvent.OnScroll -> {
|
||||||
scrollJob?.cancel()
|
scrollJob?.cancel()
|
||||||
scrollJob = launch {
|
scrollJob = launch(Dispatchers.Main) {
|
||||||
delay(300)
|
delay(300)
|
||||||
yield()
|
|
||||||
|
|
||||||
val scrollTo = (_state.value.text.lastIndex * event.progress).roundToInt()
|
val scrollTo = (_state.value.text.lastIndex * event.progress).roundToInt()
|
||||||
_state.value.listState.requestScrollToItem(scrollTo)
|
|
||||||
updateChapter(scrollTo)
|
updateChapter(scrollTo)
|
||||||
|
try {
|
||||||
|
_state.value.listState.requestScrollToItem(scrollTo)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnRestoreCheckpoint -> {
|
is ReaderEvent.OnRestoreCheckpoint -> {
|
||||||
launch {
|
launch(Dispatchers.Default) {
|
||||||
_state.value.apply {
|
_state.update {
|
||||||
listState.requestScrollToItem(
|
val checkpoints = it.checkpoints.toMutableList()
|
||||||
checkpoint.index,
|
if (checkpoints.size > 1) checkpoints.remove(event.checkpoint)
|
||||||
checkpoint.offset
|
|
||||||
)
|
|
||||||
|
|
||||||
updateChapter(checkpoint.index)
|
it.copy(
|
||||||
onEvent(
|
checkpoints = checkpoints
|
||||||
ReaderEvent.OnChangeProgress(
|
|
||||||
progress = calculateProgress(checkpoint.index),
|
|
||||||
firstVisibleItemIndex = checkpoint.index,
|
|
||||||
firstVisibleItemOffset = checkpoint.offset,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_state.value.listState.requestScrollToItem(
|
||||||
|
event.checkpoint.index,
|
||||||
|
event.checkpoint.offset
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChapter(event.checkpoint.index)
|
||||||
|
onEvent(
|
||||||
|
ReaderEvent.OnChangeProgress(
|
||||||
|
progress = calculateProgress(event.checkpoint.index),
|
||||||
|
firstVisibleItemIndex = event.checkpoint.index,
|
||||||
|
firstVisibleItemOffset = event.checkpoint.offset,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnLeave -> {
|
is ReaderEvent.OnLeave -> {
|
||||||
launch {
|
launch(Dispatchers.Main) {
|
||||||
yield()
|
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
lockMenu = true
|
lockMenu = true
|
||||||
|
|
@ -282,7 +304,7 @@ class ReaderModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnOpenTranslator -> {
|
is ReaderEvent.OnOpenTranslator -> {
|
||||||
launch {
|
launch(Dispatchers.Default) {
|
||||||
val translatorIntent = Intent()
|
val translatorIntent = Intent()
|
||||||
val browserIntent = Intent()
|
val browserIntent = Intent()
|
||||||
|
|
||||||
|
|
@ -324,7 +346,7 @@ class ReaderModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnOpenShareApp -> {
|
is ReaderEvent.OnOpenShareApp -> {
|
||||||
launch {
|
launch(Dispatchers.Default) {
|
||||||
val shareIntent = Intent()
|
val shareIntent = Intent()
|
||||||
|
|
||||||
shareIntent.action = Intent.ACTION_SEND
|
shareIntent.action = Intent.ACTION_SEND
|
||||||
|
|
@ -356,7 +378,7 @@ class ReaderModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnOpenWebBrowser -> {
|
is ReaderEvent.OnOpenWebBrowser -> {
|
||||||
launch {
|
launch(Dispatchers.Default) {
|
||||||
val browserIntent = Intent()
|
val browserIntent = Intent()
|
||||||
|
|
||||||
browserIntent.action = Intent.ACTION_WEB_SEARCH
|
browserIntent.action = Intent.ACTION_WEB_SEARCH
|
||||||
|
|
@ -382,7 +404,7 @@ class ReaderModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReaderEvent.OnOpenDictionary -> {
|
is ReaderEvent.OnOpenDictionary -> {
|
||||||
launch {
|
launch(Dispatchers.Default) {
|
||||||
val dictionaryIntent = Intent()
|
val dictionaryIntent = Intent()
|
||||||
val browserIntent = Intent()
|
val browserIntent = Intent()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,7 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
|
||||||
currentChapterProgress = state.value.currentChapterProgress,
|
currentChapterProgress = state.value.currentChapterProgress,
|
||||||
isLoading = state.value.isLoading,
|
isLoading = state.value.isLoading,
|
||||||
errorMessage = state.value.errorMessage,
|
errorMessage = state.value.errorMessage,
|
||||||
checkpoint = state.value.checkpoint,
|
checkpoints = state.value.checkpoints,
|
||||||
showMenu = state.value.showMenu,
|
showMenu = state.value.showMenu,
|
||||||
lockMenu = state.value.lockMenu,
|
lockMenu = state.value.lockMenu,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ data class ReaderState(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
|
|
||||||
val showMenu: Boolean = false,
|
val showMenu: Boolean = false,
|
||||||
val checkpoint: Checkpoint = Checkpoint(0, 0),
|
val checkpoints: List<Checkpoint> = emptyList(),
|
||||||
val lockMenu: Boolean = false,
|
val lockMenu: Boolean = false,
|
||||||
|
|
||||||
val bottomSheet: BottomSheet? = null,
|
val bottomSheet: BottomSheet? = null,
|
||||||
|
|
|
||||||
|
|
@ -6,27 +6,37 @@
|
||||||
|
|
||||||
package ua.acclorite.book_story.ui.reader
|
package ua.acclorite.book_story.ui.reader
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
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 ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
|
@ -48,34 +58,41 @@ fun ReaderBottomBar(
|
||||||
text: List<ReaderText>,
|
text: List<ReaderText>,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
lockMenu: Boolean,
|
lockMenu: Boolean,
|
||||||
checkpoint: Checkpoint,
|
checkpoints: List<Checkpoint>,
|
||||||
bottomBarPadding: Dp,
|
bottomBarPadding: Dp,
|
||||||
restoreCheckpoint: (ReaderEvent.OnRestoreCheckpoint) -> Unit,
|
restoreCheckpoint: (ReaderEvent.OnRestoreCheckpoint) -> Unit,
|
||||||
scroll: (ReaderEvent.OnScroll) -> Unit,
|
scroll: (ReaderEvent.OnScroll) -> Unit,
|
||||||
changeProgress: (ReaderEvent.OnChangeProgress) -> Unit
|
changeProgress: (ReaderEvent.OnChangeProgress) -> Unit
|
||||||
) {
|
) {
|
||||||
val firstVisibleItemIndex = remember {
|
val currentIndex by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
listState.firstVisibleItemIndex
|
listState.firstVisibleItemIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val arrowDirection = remember(checkpoint.index, firstVisibleItemIndex) {
|
val checkpointsProgress = rememberCheckpointsProgress(
|
||||||
derivedStateOf {
|
checkpoints = checkpoints,
|
||||||
val checkpointIndex = checkpoint.index
|
text = text,
|
||||||
val index = firstVisibleItemIndex.value
|
currentIndex = currentIndex
|
||||||
|
)
|
||||||
when {
|
val currentCheckpoint = rememberCurrentCheckpoint(
|
||||||
checkpointIndex > index -> Direction.END
|
checkpoints = checkpoints,
|
||||||
checkpointIndex < index -> Direction.START
|
currentIndex = currentIndex
|
||||||
else -> Direction.NEUTRAL
|
)
|
||||||
|
val checkpointDirection = rememberCheckpointDirection(
|
||||||
|
currentCheckpoint = currentCheckpoint,
|
||||||
|
currentIndex = currentIndex
|
||||||
|
)
|
||||||
|
val restoreCheckpoint = remember(currentCheckpoint) {
|
||||||
|
{
|
||||||
|
if (currentCheckpoint != null) {
|
||||||
|
restoreCheckpoint(
|
||||||
|
ReaderEvent.OnRestoreCheckpoint(
|
||||||
|
currentCheckpoint
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val checkpointProgress = remember(checkpoint.index, text.lastIndex) {
|
|
||||||
derivedStateOf {
|
|
||||||
(checkpoint.index / text.lastIndex.toFloat()) * 0.987f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -83,12 +100,11 @@ fun ReaderBottomBar(
|
||||||
.background(MaterialTheme.colorScheme.readerBarsColor)
|
.background(MaterialTheme.colorScheme.readerBarsColor)
|
||||||
.noRippleClickable(onClick = {})
|
.noRippleClickable(onClick = {})
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.padding(horizontal = 18.dp),
|
.padding(horizontal = 18.dp)
|
||||||
|
.padding(top = 16.dp, bottom = 8.dp + bottomBarPadding),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
|
|
||||||
StyledText(
|
StyledText(
|
||||||
text = progress,
|
text = progress,
|
||||||
style = MaterialTheme.typography.titleLarge.copy(
|
style = MaterialTheme.typography.titleLarge.copy(
|
||||||
|
|
@ -96,13 +112,10 @@ fun ReaderBottomBar(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(6.dp))
|
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
HorizontalExpandingTransition(
|
ReaderBottomBarCheckpoints(
|
||||||
visible = arrowDirection.value == Direction.START,
|
checkpointDirection = checkpointDirection,
|
||||||
startDirection = true
|
startArrow = {
|
||||||
) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon = Icons.AutoMirrored.Default.ArrowBack,
|
icon = Icons.AutoMirrored.Default.ArrowBack,
|
||||||
contentDescription = R.string.checkpoint_back_content_desc,
|
contentDescription = R.string.checkpoint_back_content_desc,
|
||||||
|
|
@ -110,31 +123,10 @@ fun ReaderBottomBar(
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
disableOnClick = false
|
disableOnClick = false
|
||||||
) {
|
) {
|
||||||
restoreCheckpoint(ReaderEvent.OnRestoreCheckpoint)
|
restoreCheckpoint()
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
endArrow = {
|
||||||
Box(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
contentAlignment = Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
ReaderBottomBarSlider(
|
|
||||||
book = book,
|
|
||||||
lockMenu = lockMenu,
|
|
||||||
listState = listState,
|
|
||||||
scroll = scroll,
|
|
||||||
changeProgress = changeProgress
|
|
||||||
)
|
|
||||||
|
|
||||||
if (arrowDirection.value != Direction.NEUTRAL) {
|
|
||||||
ReaderBottomBarSliderIndicator(progress = checkpointProgress.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalExpandingTransition(
|
|
||||||
visible = arrowDirection.value == Direction.END,
|
|
||||||
startDirection = false
|
|
||||||
) {
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon = Icons.AutoMirrored.Default.ArrowForward,
|
icon = Icons.AutoMirrored.Default.ArrowForward,
|
||||||
contentDescription = R.string.checkpoint_forward_content_desc,
|
contentDescription = R.string.checkpoint_forward_content_desc,
|
||||||
|
|
@ -142,11 +134,160 @@ fun ReaderBottomBar(
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
disableOnClick = false
|
disableOnClick = false
|
||||||
) {
|
) {
|
||||||
restoreCheckpoint(ReaderEvent.OnRestoreCheckpoint)
|
restoreCheckpoint()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
slider = {
|
||||||
|
ReaderBottomBarSlider(
|
||||||
|
book = book,
|
||||||
|
lockMenu = lockMenu,
|
||||||
|
listState = listState,
|
||||||
|
scroll = scroll,
|
||||||
|
changeProgress = changeProgress
|
||||||
|
)
|
||||||
|
},
|
||||||
|
indicator = {
|
||||||
|
ReaderBottomBarCheckpointsIndicator(
|
||||||
|
checkpointsProgress = checkpointsProgress
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberCheckpointsProgress(
|
||||||
|
checkpoints: List<Checkpoint>,
|
||||||
|
text: List<ReaderText>,
|
||||||
|
currentIndex: Int
|
||||||
|
): List<Float> {
|
||||||
|
return remember(checkpoints, text, currentIndex) {
|
||||||
|
if (text.isEmpty()) return@remember emptyList()
|
||||||
|
|
||||||
|
val progressScale = 0.987f
|
||||||
|
checkpoints.mapNotNull { checkpoint ->
|
||||||
|
if (checkpoint.index == currentIndex) return@mapNotNull null
|
||||||
|
(checkpoint.index / text.lastIndex.toFloat()) * progressScale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberCurrentCheckpoint(
|
||||||
|
checkpoints: List<Checkpoint>,
|
||||||
|
currentIndex: Int
|
||||||
|
): Checkpoint? {
|
||||||
|
return remember(checkpoints, currentIndex) {
|
||||||
|
checkpoints.lastOrNull { checkpoint ->
|
||||||
|
when {
|
||||||
|
checkpoint.index > currentIndex -> true
|
||||||
|
checkpoint.index < currentIndex -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberCheckpointDirection(
|
||||||
|
currentCheckpoint: Checkpoint?,
|
||||||
|
currentIndex: Int
|
||||||
|
): Direction {
|
||||||
|
return remember(currentCheckpoint, currentIndex) {
|
||||||
|
when {
|
||||||
|
currentCheckpoint == null -> Direction.NEUTRAL
|
||||||
|
currentCheckpoint.index > currentIndex -> Direction.END
|
||||||
|
currentCheckpoint.index < currentIndex -> Direction.START
|
||||||
|
else -> Direction.NEUTRAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.ReaderBottomBarCheckpoints(
|
||||||
|
checkpointDirection: Direction,
|
||||||
|
startArrow: @Composable () -> Unit,
|
||||||
|
endArrow: @Composable () -> Unit,
|
||||||
|
slider: @Composable () -> Unit,
|
||||||
|
indicator: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
HorizontalExpandingTransition(
|
||||||
|
visible = checkpointDirection == Direction.START,
|
||||||
|
startDirection = true
|
||||||
|
) {
|
||||||
|
startArrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
slider()
|
||||||
|
indicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalExpandingTransition(
|
||||||
|
visible = checkpointDirection == Direction.END,
|
||||||
|
startDirection = false
|
||||||
|
) {
|
||||||
|
endArrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderBottomBarSlider(
|
||||||
|
book: Book,
|
||||||
|
lockMenu: Boolean,
|
||||||
|
listState: LazyListState,
|
||||||
|
scroll: (ReaderEvent.OnScroll) -> Unit,
|
||||||
|
changeProgress: (ReaderEvent.OnChangeProgress) -> Unit
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isDragging = interactionSource.collectIsDraggedAsState()
|
||||||
|
val animatedProgress = animateFloatAsState(book.progress)
|
||||||
|
val progress = remember(isDragging.value, book.progress, animatedProgress.value) {
|
||||||
|
if (isDragging.value) book.progress
|
||||||
|
else animatedProgress.value
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = progress,
|
||||||
|
enabled = !lockMenu,
|
||||||
|
onValueChange = {
|
||||||
|
if (listState.layoutInfo.totalItemsCount > 0) {
|
||||||
|
scroll(ReaderEvent.OnScroll(it))
|
||||||
|
changeProgress(
|
||||||
|
ReaderEvent.OnChangeProgress(
|
||||||
|
progress = it,
|
||||||
|
firstVisibleItemIndex = listState.firstVisibleItemIndex,
|
||||||
|
firstVisibleItemOffset = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f),
|
||||||
|
disabledActiveTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
disabledThumbColor = MaterialTheme.colorScheme.primary,
|
||||||
|
disabledInactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderBottomBarCheckpointsIndicator(checkpointsProgress: List<Float>) {
|
||||||
|
checkpointsProgress.forEach { checkpoint ->
|
||||||
|
Row {
|
||||||
|
Spacer(modifier = Modifier.fillMaxWidth(checkpoint))
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.width(4.dp)
|
||||||
|
.height(16.dp)
|
||||||
|
.clip(RoundedCornerShape(0.5.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.onPrimary.copy(0.7f))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp + bottomBarPadding))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* Book's Story — free and open-source Material You eBook reader.
|
|
||||||
* Copyright (C) 2024-2025 Acclorite
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ua.acclorite.book_story.ui.reader
|
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.SliderDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import ua.acclorite.book_story.domain.model.library.Book
|
|
||||||
import ua.acclorite.book_story.presentation.reader.ReaderEvent
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ReaderBottomBarSlider(
|
|
||||||
book: Book,
|
|
||||||
lockMenu: Boolean,
|
|
||||||
listState: LazyListState,
|
|
||||||
scroll: (ReaderEvent.OnScroll) -> Unit,
|
|
||||||
changeProgress: (ReaderEvent.OnChangeProgress) -> Unit
|
|
||||||
) {
|
|
||||||
Slider(
|
|
||||||
value = book.progress,
|
|
||||||
enabled = !lockMenu,
|
|
||||||
onValueChange = {
|
|
||||||
if (listState.layoutInfo.totalItemsCount > 0) {
|
|
||||||
scroll(ReaderEvent.OnScroll(it))
|
|
||||||
changeProgress(
|
|
||||||
ReaderEvent.OnChangeProgress(
|
|
||||||
progress = it,
|
|
||||||
firstVisibleItemIndex = listState.firstVisibleItemIndex,
|
|
||||||
firstVisibleItemOffset = 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = SliderDefaults.colors(
|
|
||||||
inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f),
|
|
||||||
disabledActiveTrackColor = MaterialTheme.colorScheme.primary,
|
|
||||||
disabledThumbColor = MaterialTheme.colorScheme.primary,
|
|
||||||
disabledInactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
* Book's Story — free and open-source Material You eBook reader.
|
|
||||||
* Copyright (C) 2024-2025 Acclorite
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package ua.acclorite.book_story.ui.reader
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ReaderBottomBarSliderIndicator(progress: Float) {
|
|
||||||
Row(Modifier.fillMaxWidth()) {
|
|
||||||
Spacer(
|
|
||||||
modifier = Modifier.fillMaxWidth(progress)
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.width(4.dp)
|
|
||||||
.height(16.dp)
|
|
||||||
.clip(RoundedCornerShape(0.5.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.onPrimary.copy(0.6f))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -49,7 +49,7 @@ fun ReaderContent(
|
||||||
currentChapterProgress: Float,
|
currentChapterProgress: Float,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
errorMessage: UIText?,
|
errorMessage: UIText?,
|
||||||
checkpoint: Checkpoint,
|
checkpoints: List<Checkpoint>,
|
||||||
showMenu: Boolean,
|
showMenu: Boolean,
|
||||||
lockMenu: Boolean,
|
lockMenu: Boolean,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
|
|
@ -128,7 +128,7 @@ fun ReaderContent(
|
||||||
perceptionExpanderThickness = perceptionExpanderThickness,
|
perceptionExpanderThickness = perceptionExpanderThickness,
|
||||||
currentChapterProgress = currentChapterProgress,
|
currentChapterProgress = currentChapterProgress,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
checkpoint = checkpoint,
|
checkpoints = checkpoints,
|
||||||
showMenu = showMenu,
|
showMenu = showMenu,
|
||||||
lockMenu = lockMenu,
|
lockMenu = lockMenu,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ fun ReaderScaffold(
|
||||||
perceptionExpanderThickness: Dp,
|
perceptionExpanderThickness: Dp,
|
||||||
currentChapterProgress: Float,
|
currentChapterProgress: Float,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
checkpoint: Checkpoint,
|
checkpoints: List<Checkpoint>,
|
||||||
showMenu: Boolean,
|
showMenu: Boolean,
|
||||||
lockMenu: Boolean,
|
lockMenu: Boolean,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
|
|
@ -150,7 +150,7 @@ fun ReaderScaffold(
|
||||||
text = text,
|
text = text,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
lockMenu = lockMenu,
|
lockMenu = lockMenu,
|
||||||
checkpoint = checkpoint,
|
checkpoints = checkpoints,
|
||||||
bottomBarPadding = bottomBarPadding,
|
bottomBarPadding = bottomBarPadding,
|
||||||
restoreCheckpoint = restoreCheckpoint,
|
restoreCheckpoint = restoreCheckpoint,
|
||||||
scroll = scroll,
|
scroll = scroll,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue