🚀 Reader horizontal gesture option

* Temporary replacement of Paged Layout
* Scrolls by specified amount (scroll fraction) after dragging horizontally
* Fade + Offset animation to visualize "dragging"
* By default is turned off
* Placed in the new Reading Mode subcategory of Reader Settings

Resolves: #127
This commit is contained in:
Acclorite 2024-12-24 14:52:43 +02:00
parent dd3fc17a6f
commit ba2104ad05
17 changed files with 330 additions and 1 deletions

View file

@ -0,0 +1,11 @@
package ua.acclorite.book_story.domain.reader
enum class ReaderHorizontalGesture {
OFF,
ON,
INVERSE
}
fun String.toHorizontalGesture(): ReaderHorizontalGesture {
return ReaderHorizontalGesture.valueOf(this)
}

View file

@ -43,6 +43,8 @@ object DataStoreConstants {
val SCREEN_ORIENTATION = stringPreferencesKey("screen_orientation")
val CUSTOM_SCREEN_BRIGHTNESS = booleanPreferencesKey("custom_screen_brightness")
val SCREEN_BRIGHTNESS = doublePreferencesKey("screen_brightness")
val HORIZONTAL_GESTURE = stringPreferencesKey("horizontal_gesture")
val HORIZONTAL_GESTURE_SCROLL = doublePreferencesKey("horizontal_gesture_scroll")
// Browse settings
val BROWSE_FILES_STRUCTURE = stringPreferencesKey("browse_files_structure")

View file

@ -13,6 +13,7 @@ import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.Checkpoint
import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.domain.util.BottomSheet
@ -46,6 +47,8 @@ fun ReaderContent(
chapters: Map<Int, Chapter>,
contentPadding: PaddingValues,
verticalPadding: Dp,
horizontalGesture: ReaderHorizontalGesture,
horizontalGestureScroll: Float,
paragraphHeight: Dp,
sidePadding: Dp,
backgroundColor: Color,
@ -117,6 +120,8 @@ fun ReaderContent(
chapters = chapters,
contentPadding = contentPadding,
verticalPadding = verticalPadding,
horizontalGesture = horizontalGesture,
horizontalGestureScroll = horizontalGestureScroll,
paragraphHeight = paragraphHeight,
sidePadding = sidePadding,
backgroundColor = backgroundColor,

View file

@ -0,0 +1,108 @@
package ua.acclorite.book_story.presentation.reader
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import kotlin.math.abs
import kotlin.math.roundToInt
@Composable
fun Modifier.readerHorizontalGesture(
listState: LazyListState,
horizontalGesture: ReaderHorizontalGesture,
horizontalGestureScroll: Float,
isLoading: Boolean
): Modifier {
if (horizontalGesture == ReaderHorizontalGesture.OFF || isLoading) return this
val inverted = rememberUpdatedState(horizontalGesture == ReaderHorizontalGesture.INVERSE)
val scope = rememberCoroutineScope()
val density = LocalDensity.current
val screenHeight = remember {
mutableFloatStateOf(0f)
}
fun scrollBackward() {
scope.launch {
listState.scrollBy(-screenHeight.floatValue)
}
}
fun scrollForward() {
scope.launch {
listState.scrollBy(screenHeight.floatValue)
}
}
val horizontalOffset = remember {
mutableFloatStateOf(0f)
}
val animatedHorizontalOffset = animateFloatAsState(
horizontalOffset.floatValue
)
@Composable
fun calculateAlpha(): Float {
return animateFloatAsState(
1f - (abs(with(density) { horizontalOffset.floatValue.toDp() }.value) / 16f)
).value
}
return this
.onGloballyPositioned {
screenHeight.floatValue = it.size.height * horizontalGestureScroll
}
.alpha(calculateAlpha())
.offset {
IntOffset(
x = animatedHorizontalOffset.value.roundToInt(),
y = 0
)
}
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragStart = {
horizontalOffset.floatValue = 0f
},
onHorizontalDrag = { change, dragAmount ->
horizontalOffset.floatValue += (dragAmount * 0.15f)
},
onDragCancel = {
horizontalOffset.floatValue = 0f
},
onDragEnd = {
val horizontalOffsetDp = with(density) { horizontalOffset.floatValue.toDp() }
when {
horizontalOffsetDp > 16.dp -> {
if (inverted.value) scrollForward()
else scrollBackward()
}
horizontalOffsetDp < (-16).dp -> {
if (inverted.value) scrollBackward()
else scrollForward()
}
}
horizontalOffset.floatValue = 0f
}
)
}
}

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.presentation.core.components.common.LazyColumnWithScrollbar
import ua.acclorite.book_story.presentation.core.components.common.SelectionContainer
@ -38,6 +39,8 @@ fun ReaderLayout(
listState: LazyListState,
contentPadding: PaddingValues,
verticalPadding: Dp,
horizontalGesture: ReaderHorizontalGesture,
horizontalGestureScroll: Float,
paragraphHeight: Dp,
sidePadding: Dp,
backgroundColor: Color,
@ -124,7 +127,13 @@ fun ReaderLayout(
} else Modifier
)
.padding(contentPadding)
.padding(vertical = verticalPadding),
.padding(vertical = verticalPadding)
.readerHorizontalGesture(
listState = listState,
horizontalGesture = horizontalGesture,
horizontalGestureScroll = horizontalGestureScroll,
isLoading = isLoading
),
verticalArrangement = Arrangement.spacedBy(paragraphHeight),
contentPadding = PaddingValues(
top = (WindowInsets.displayCutout.asPaddingValues()

View file

@ -22,6 +22,7 @@ import ua.acclorite.book_story.domain.library.book.Book
import ua.acclorite.book_story.domain.reader.Chapter
import ua.acclorite.book_story.domain.reader.Checkpoint
import ua.acclorite.book_story.domain.reader.FontWithName
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.domain.ui.UIText
import ua.acclorite.book_story.presentation.core.components.common.AnimatedVisibility
@ -51,6 +52,8 @@ fun ReaderScaffold(
chapters: Map<Int, Chapter>,
contentPadding: PaddingValues,
verticalPadding: Dp,
horizontalGesture: ReaderHorizontalGesture,
horizontalGestureScroll: Float,
paragraphHeight: Dp,
sidePadding: Dp,
backgroundColor: Color,
@ -142,6 +145,8 @@ fun ReaderScaffold(
listState = listState,
contentPadding = contentPadding,
verticalPadding = verticalPadding,
horizontalGesture = horizontalGesture,
horizontalGestureScroll = horizontalGestureScroll,
paragraphHeight = paragraphHeight,
sidePadding = sidePadding,
backgroundColor = backgroundColor,

View file

@ -27,6 +27,7 @@ import ua.acclorite.book_story.presentation.settings.appearance.colors.ColorsSub
import ua.acclorite.book_story.presentation.settings.reader.font.FontSubcategory
import ua.acclorite.book_story.presentation.settings.reader.misc.MiscSubcategory
import ua.acclorite.book_story.presentation.settings.reader.padding.PaddingSubcategory
import ua.acclorite.book_story.presentation.settings.reader.reading_mode.ReadingModeSubcategory
import ua.acclorite.book_story.presentation.settings.reader.reading_speed.ReadingSpeedSubcategory
import ua.acclorite.book_story.presentation.settings.reader.system.SystemSubcategory
import ua.acclorite.book_story.presentation.settings.reader.text.TextSubcategory
@ -93,6 +94,11 @@ fun ReaderSettingsBottomSheet(
when (page) {
0 -> {
LazyColumnWithScrollbar(Modifier.fillMaxSize()) {
ReadingModeSubcategory(
titleColor = { MaterialTheme.colorScheme.onSurface },
topPadding = 22.dp,
bottomPadding = 0.dp
)
PaddingSubcategory(
titleColor = { MaterialTheme.colorScheme.onSurface },
topPadding = 22.dp,

View file

@ -11,6 +11,7 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.settings.reader.font.FontSubcategory
import ua.acclorite.book_story.presentation.settings.reader.misc.MiscSubcategory
import ua.acclorite.book_story.presentation.settings.reader.padding.PaddingSubcategory
import ua.acclorite.book_story.presentation.settings.reader.reading_mode.ReadingModeSubcategory
import ua.acclorite.book_story.presentation.settings.reader.reading_speed.ReadingSpeedSubcategory
import ua.acclorite.book_story.presentation.settings.reader.system.SystemSubcategory
import ua.acclorite.book_story.presentation.settings.reader.text.TextSubcategory
@ -31,6 +32,11 @@ fun LazyListScope.ReaderSettingsCategory(
topPadding = 22.dp,
bottomPadding = 0.dp
)
ReadingModeSubcategory(
titleColor = titleColor,
topPadding = 22.dp,
bottomPadding = 0.dp
)
PaddingSubcategory(
titleColor = titleColor,
topPadding = 22.dp,

View file

@ -0,0 +1,40 @@
@file:Suppress("FunctionName")
package ua.acclorite.book_story.presentation.settings.reader.reading_mode
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.settings.components.SettingsSubcategory
import ua.acclorite.book_story.presentation.settings.reader.reading_mode.components.HorizontalGestureOption
import ua.acclorite.book_story.presentation.settings.reader.reading_mode.components.HorizontalGestureScrollOption
fun LazyListScope.ReadingModeSubcategory(
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
title: @Composable () -> String = { stringResource(id = R.string.reading_mode_reader_settings) },
showTitle: Boolean = true,
showDivider: Boolean = true,
topPadding: Dp,
bottomPadding: Dp
) {
SettingsSubcategory(
titleColor = titleColor,
title = title,
showTitle = showTitle,
showDivider = showDivider,
topPadding = topPadding,
bottomPadding = bottomPadding
) {
item {
HorizontalGestureOption()
}
item {
HorizontalGestureScrollOption()
}
}
}

View file

@ -0,0 +1,50 @@
package ua.acclorite.book_story.presentation.settings.reader.reading_mode.components
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.ui.ButtonItem
import ua.acclorite.book_story.presentation.core.components.settings.ChipsWithTitle
import ua.acclorite.book_story.ui.main.MainEvent
import ua.acclorite.book_story.ui.main.MainModel
@Composable
fun HorizontalGestureOption() {
val mainModel = hiltViewModel<MainModel>()
val state = mainModel.state.collectAsStateWithLifecycle()
ChipsWithTitle(
title = stringResource(id = R.string.horizontal_gesture_option),
chips = ReaderHorizontalGesture.entries.map {
ButtonItem(
id = it.toString(),
title = when (it) {
ReaderHorizontalGesture.OFF -> {
stringResource(R.string.horizontal_gesture_off)
}
ReaderHorizontalGesture.ON -> {
stringResource(R.string.horizontal_gesture_on)
}
ReaderHorizontalGesture.INVERSE -> {
stringResource(R.string.horizontal_gesture_inverse)
}
},
textStyle = MaterialTheme.typography.labelLarge,
selected = it == state.value.horizontalGesture
)
},
onClick = {
mainModel.onEvent(
MainEvent.OnChangeHorizontalGesture(
it.id
)
)
}
)
}

View file

@ -0,0 +1,36 @@
package ua.acclorite.book_story.presentation.settings.reader.reading_mode.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.presentation.core.components.settings.SliderWithTitle
import ua.acclorite.book_story.ui.main.MainEvent
import ua.acclorite.book_story.ui.main.MainModel
import ua.acclorite.book_story.ui.theme.ExpandingTransition
@Composable
fun HorizontalGestureScrollOption() {
val mainModel = hiltViewModel<MainModel>()
val state = mainModel.state.collectAsStateWithLifecycle()
ExpandingTransition(
visible = when (state.value.horizontalGesture) {
ReaderHorizontalGesture.OFF -> false
else -> true
}
) {
SliderWithTitle(
value = state.value.horizontalGestureScroll to "%",
toValue = 100,
title = stringResource(id = R.string.horizontal_gesture_scroll_option),
onValueChange = {
mainModel.onEvent(
MainEvent.OnChangeHorizontalGestureScroll(it)
)
}
)
}
}

View file

@ -45,4 +45,6 @@ sealed class MainEvent {
data class OnChangeScreenOrientation(val value: String) : MainEvent()
data class OnChangeCustomScreenBrightness(val value: Boolean) : MainEvent()
data class OnChangeScreenBrightness(val value: Float) : MainEvent()
data class OnChangeHorizontalGesture(val value: String) : MainEvent()
data class OnChangeHorizontalGestureScroll(val value: Float) : MainEvent()
}

View file

@ -17,6 +17,7 @@ import kotlinx.coroutines.withContext
import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure
import ua.acclorite.book_story.domain.browse.toBrowseLayout
import ua.acclorite.book_story.domain.browse.toBrowseSortOrder
import ua.acclorite.book_story.domain.reader.toHorizontalGesture
import ua.acclorite.book_story.domain.reader.toReaderScreenOrientation
import ua.acclorite.book_story.domain.reader.toTextAlignment
import ua.acclorite.book_story.domain.use_case.data_store.ChangeLanguage
@ -377,6 +378,22 @@ class MainModel @Inject constructor(
it.copy(screenBrightness = this.toFloat())
}
)
is MainEvent.OnChangeHorizontalGesture -> handleDatastoreUpdate(
key = DataStoreConstants.HORIZONTAL_GESTURE,
value = event.value,
updateState = {
it.copy(horizontalGesture = toHorizontalGesture())
}
)
is MainEvent.OnChangeHorizontalGestureScroll -> handleDatastoreUpdate(
key = DataStoreConstants.HORIZONTAL_GESTURE_SCROLL,
value = event.value.toDouble(),
updateState = {
it.copy(horizontalGestureScroll = this.toFloat())
}
)
}
}

View file

@ -13,8 +13,10 @@ import ua.acclorite.book_story.domain.browse.BrowseSortOrder
import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure
import ua.acclorite.book_story.domain.browse.toBrowseLayout
import ua.acclorite.book_story.domain.browse.toBrowseSortOrder
import ua.acclorite.book_story.domain.reader.ReaderHorizontalGesture
import ua.acclorite.book_story.domain.reader.ReaderScreenOrientation
import ua.acclorite.book_story.domain.reader.ReaderTextAlignment
import ua.acclorite.book_story.domain.reader.toHorizontalGesture
import ua.acclorite.book_story.domain.reader.toReaderScreenOrientation
import ua.acclorite.book_story.domain.reader.toTextAlignment
import ua.acclorite.book_story.presentation.core.constants.Constants
@ -84,6 +86,10 @@ data class MainState(
},
val customScreenBrightness: Boolean = provideDefaultValue { false },
val screenBrightness: Float = provideDefaultValue { 0.5f },
val horizontalGesture: ReaderHorizontalGesture = provideDefaultValue {
ReaderHorizontalGesture.OFF
},
val horizontalGestureScroll: Float = provideDefaultValue { 0.7f },
// Browse Settings
val browseFilesStructure: BrowseFilesStructure = provideDefaultValue {
@ -281,6 +287,14 @@ data class MainState(
screenBrightness = provideValue(
SCREEN_BRIGHTNESS, convert = { this.toFloat() }
) { screenBrightness },
horizontalGesture = provideValue(
HORIZONTAL_GESTURE, convert = { toHorizontalGesture() }
) { horizontalGesture },
horizontalGestureScroll = provideValue(
HORIZONTAL_GESTURE_SCROLL, convert = { toFloat() }
) { horizontalGestureScroll },
)
}
}

View file

@ -316,6 +316,8 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
chapters = chapters,
contentPadding = contentPadding,
verticalPadding = verticalPadding,
horizontalGesture = mainState.value.horizontalGesture,
horizontalGestureScroll = mainState.value.horizontalGestureScroll,
paragraphHeight = paragraphHeight,
sidePadding = sidePadding,
backgroundColor = backgroundColor.value,

View file

@ -207,6 +207,7 @@
<string name="padding_reader_settings">Відступи</string>
<string name="misc_reader_settings">Інше</string>
<string name="reading_speed_reader_settings">Швидкість читання</string>
<string name="reading_mode_reader_settings">Режим читання</string>
<string name="system_reader_settings">Система</string>
<string name="theme_appearance_settings">Налаштування теми</string>
<string name="colors_appearance_settings">Кольори</string>
@ -264,6 +265,8 @@
<string name="screen_orientation_option">Орієнтація екрана</string>
<string name="custom_screen_brightness_option">Користувацька яскравість</string>
<string name="screen_brightness_option">Яскравість</string>
<string name="horizontal_gesture_option">Горизонтальний жест</string>
<string name="horizontal_gesture_scroll_option">Частка прокрутки</string>
<!-- Browse -->
<string name="browse_files_structure_option">Структура файлів</string>
@ -331,6 +334,11 @@
<string name="screen_orientation_locked_portrait">Заблокований портрет</string>
<string name="screen_orientation_locked_landscape">Заблокований альбом</string>
<!-- Horizontal Gesture properties -->
<string name="horizontal_gesture_off">Вимкнений</string>
<string name="horizontal_gesture_on">Ввімкнений</string>
<string name="horizontal_gesture_inverse">Ввімкнений (Інвертований)</string>
<!-- Themes -->
<string name="dynamic_theme">Меркурій</string>
<string name="blue_theme">Нептун</string>

View file

@ -208,6 +208,7 @@
<string name="padding_reader_settings">Padding</string>
<string name="misc_reader_settings">Misc</string>
<string name="reading_speed_reader_settings">Reading speed</string>
<string name="reading_mode_reader_settings">Reading mode</string>
<string name="system_reader_settings">System</string>
<string name="theme_appearance_settings">Theme preferences</string>
<string name="colors_appearance_settings">Colors</string>
@ -265,6 +266,8 @@
<string name="screen_orientation_option">Screen orientation</string>
<string name="custom_screen_brightness_option">Custom brightness</string>
<string name="screen_brightness_option">Brightness</string>
<string name="horizontal_gesture_option">Horizontal gesture</string>
<string name="horizontal_gesture_scroll_option">Scroll fraction</string>
<!-- Browse -->
<string name="browse_files_structure_option">Files structure</string>
@ -332,6 +335,11 @@
<string name="screen_orientation_locked_portrait">Locked portrait</string>
<string name="screen_orientation_locked_landscape">Locked landscape</string>
<!-- Horizontal Gesture properties -->
<string name="horizontal_gesture_off">Off</string>
<string name="horizontal_gesture_on">On</string>
<string name="horizontal_gesture_inverse">On (Inverse)</string>
<!-- Themes -->
<string name="dynamic_theme">Mercury</string>
<string name="blue_theme">Neptune</string>