feat: add "horizontal limiter" option

* Option to show horizontal ruler and dim unfocused part of the screen (both height of the focused part and its vertical position can be customized)
* Option to hide horizontal ruler
* Option to change horizontal ruler's thickness
* Option to change dimming (0-100%)

Resolves: #197
This commit is contained in:
Acclorite 2025-12-26 13:22:51 +02:00
parent ffa249ab78
commit 959a193e52
No known key found for this signature in database
GPG key ID: 6E54C611F6EE8593
14 changed files with 387 additions and 0 deletions

View file

@ -180,6 +180,26 @@ class SettingsManager @Inject constructor(
val perceptionExpanderThickness = setting<Int, Int>( val perceptionExpanderThickness = setting<Int, Int>(
key = intPreferencesKey("perception_expander_thickness"), default = 4 key = intPreferencesKey("perception_expander_thickness"), default = 4
) )
val horizontalLimiter = setting<Boolean, Boolean>(
key = booleanPreferencesKey("horizontal_limiter"), default = false
)
val horizontalLimiterHeight = setting<Int, Int>(
key = intPreferencesKey("horizontal_limiter_height"), default = 6
)
val horizontalLimiterVerticalOffset = setting<Float, Double>(
key = doublePreferencesKey("horizontal_limiter_vertical_offset"), default = 0.5f,
serialize = { it.toDouble() }, deserialize = { it.toFloat() }
)
val horizontalLimiterRuler = setting<Boolean, Boolean>(
key = booleanPreferencesKey("horizontal_limiter_ruler"), default = true
)
val horizontalLimiterRulerThickness = setting<Int, Int>(
key = intPreferencesKey("horizontal_limiter_ruler_thickness"), default = 4
)
val horizontalLimiterDimming = setting<Float, Double>(
key = doublePreferencesKey("horizontal_limiter_dimming"), default = 0.0f,
serialize = { it.toDouble() }, deserialize = { it.toFloat() }
)
val screenOrientation = setting<ReaderScreenOrientation, String>( val screenOrientation = setting<ReaderScreenOrientation, String>(
key = stringPreferencesKey("screen_orientation"), default = ReaderScreenOrientation.DEFAULT, key = stringPreferencesKey("screen_orientation"), default = ReaderScreenOrientation.DEFAULT,
serialize = { it.name }, deserialize = { ReaderScreenOrientation.valueOf(it) } serialize = { it.name }, deserialize = { ReaderScreenOrientation.valueOf(it) }

View file

@ -166,6 +166,16 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
) { ) {
(settings.perceptionExpanderThickness.lastValue * 0.25f).dp (settings.perceptionExpanderThickness.lastValue * 0.25f).dp
} }
val horizontalLimiterHeight = remember(
settings.horizontalLimiterHeight.value
) {
(settings.horizontalLimiterHeight.lastValue * 20).dp
}
val horizontalLimiterRulerThickness = remember(
settings.horizontalLimiterRulerThickness.value
) {
(settings.horizontalLimiterRulerThickness.lastValue * 0.25f).dp
}
val horizontalGestureSensitivity = remember(settings.horizontalGestureSensitivity.value) { val horizontalGestureSensitivity = remember(settings.horizontalGestureSensitivity.value) {
(36f + settings.horizontalGestureSensitivity.lastValue * (4f - 36f)).dp (36f + settings.horizontalGestureSensitivity.lastValue * (4f - 36f)).dp
} }
@ -395,6 +405,12 @@ data class ReaderScreen(val bookId: Int) : Screen, Parcelable {
perceptionExpander = settings.perceptionExpander.value, perceptionExpander = settings.perceptionExpander.value,
perceptionExpanderPadding = perceptionExpanderPadding, perceptionExpanderPadding = perceptionExpanderPadding,
perceptionExpanderThickness = perceptionExpanderThickness, perceptionExpanderThickness = perceptionExpanderThickness,
horizontalLimiter = settings.horizontalLimiter.value,
horizontalLimiterHeight = horizontalLimiterHeight,
horizontalLimiterVerticalOffset = settings.horizontalLimiterVerticalOffset.value,
horizontalLimiterRuler = settings.horizontalLimiterRuler.value,
horizontalLimiterRulerThickness = horizontalLimiterRulerThickness,
horizontalLimiterDimming = settings.horizontalLimiterDimming.value,
currentChapterProgress = state.value.currentChapterProgress, currentChapterProgress = state.value.currentChapterProgress,
isLoading = state.value.isLoading, isLoading = state.value.isLoading,
errorMessage = state.value.errorMessage, errorMessage = state.value.errorMessage,

View file

@ -46,6 +46,12 @@ fun ReaderContent(
perceptionExpander: Boolean, perceptionExpander: Boolean,
perceptionExpanderPadding: Dp, perceptionExpanderPadding: Dp,
perceptionExpanderThickness: Dp, perceptionExpanderThickness: Dp,
horizontalLimiter: Boolean,
horizontalLimiterHeight: Dp,
horizontalLimiterVerticalOffset: Float,
horizontalLimiterRulerThickness: Dp,
horizontalLimiterRuler: Boolean,
horizontalLimiterDimming: Float,
currentChapterProgress: Float, currentChapterProgress: Float,
isLoading: Boolean, isLoading: Boolean,
errorMessage: UIText?, errorMessage: UIText?,
@ -124,6 +130,12 @@ fun ReaderContent(
perceptionExpander = perceptionExpander, perceptionExpander = perceptionExpander,
perceptionExpanderPadding = perceptionExpanderPadding, perceptionExpanderPadding = perceptionExpanderPadding,
perceptionExpanderThickness = perceptionExpanderThickness, perceptionExpanderThickness = perceptionExpanderThickness,
horizontalLimiter = horizontalLimiter,
horizontalLimiterHeight = horizontalLimiterHeight,
horizontalLimiterVerticalOffset = horizontalLimiterVerticalOffset,
horizontalLimiterRuler = horizontalLimiterRuler,
horizontalLimiterRulerThickness = horizontalLimiterRulerThickness,
horizontalLimiterDimming = horizontalLimiterDimming,
currentChapterProgress = currentChapterProgress, currentChapterProgress = currentChapterProgress,
isLoading = isLoading, isLoading = isLoading,
checkpoints = checkpoints, checkpoints = checkpoints,

View file

@ -0,0 +1,96 @@
/*
* 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.Dp
import ua.acclorite.book_story.ui.theme.DefaultTransition
@Composable
fun ReaderHorizontalLimiter(
horizontalLimiter: Boolean,
horizontalLimiterHeight: Dp,
horizontalLimiterVerticalOffset: Float,
horizontalLimiterRulerThickness: Dp,
horizontalLimiterRuler: Boolean,
horizontalLimiterDimming: Float,
horizontalLimiterDimmingColor: Color,
horizontalLimiterRulerColor: Color
) {
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current
val containerHeight = remember(density, windowInfo.containerSize, horizontalLimiterHeight) {
with(density) {
windowInfo.containerSize.height.toDp() - horizontalLimiterHeight
}
}
val verticalLimiterTopOffsetDp = remember(
containerHeight,
horizontalLimiterVerticalOffset
) {
containerHeight * horizontalLimiterVerticalOffset
}
val verticalLimiterBottomOffsetDp = remember(
containerHeight,
horizontalLimiterVerticalOffset
) {
containerHeight * (1 - horizontalLimiterVerticalOffset)
}
DefaultTransition(horizontalLimiter) {
Column(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(verticalLimiterTopOffsetDp)
.background(horizontalLimiterDimmingColor.copy(alpha = horizontalLimiterDimming))
)
Column(
modifier = Modifier
.height(
horizontalLimiterHeight.coerceAtMost(
with(density) { windowInfo.containerSize.height.toDp() }
)
),
verticalArrangement = Arrangement.SpaceBetween
) {
if (horizontalLimiterRuler) {
repeat(2) {
HorizontalDivider(
modifier = Modifier.fillMaxWidth(),
horizontalLimiterRulerThickness,
horizontalLimiterRulerColor
)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(verticalLimiterBottomOffsetDp)
.background(horizontalLimiterDimmingColor.copy(alpha = horizontalLimiterDimming))
)
}
}
}

View file

@ -51,6 +51,12 @@ fun ReaderScaffold(
perceptionExpander: Boolean, perceptionExpander: Boolean,
perceptionExpanderPadding: Dp, perceptionExpanderPadding: Dp,
perceptionExpanderThickness: Dp, perceptionExpanderThickness: Dp,
horizontalLimiter: Boolean,
horizontalLimiterHeight: Dp,
horizontalLimiterVerticalOffset: Float,
horizontalLimiterRulerThickness: Dp,
horizontalLimiterRuler: Boolean,
horizontalLimiterDimming: Float,
currentChapterProgress: Float, currentChapterProgress: Float,
isLoading: Boolean, isLoading: Boolean,
checkpoints: List<Checkpoint>, checkpoints: List<Checkpoint>,
@ -212,6 +218,17 @@ fun ReaderScaffold(
perceptionExpanderColor = fontColor perceptionExpanderColor = fontColor
) )
ReaderHorizontalLimiter(
horizontalLimiter = horizontalLimiter,
horizontalLimiterHeight = horizontalLimiterHeight,
horizontalLimiterVerticalOffset = horizontalLimiterVerticalOffset,
horizontalLimiterRulerThickness = horizontalLimiterRulerThickness,
horizontalLimiterRuler = horizontalLimiterRuler,
horizontalLimiterDimming = horizontalLimiterDimming,
horizontalLimiterDimmingColor = backgroundColor,
horizontalLimiterRulerColor = fontColor
)
if (isLoading) { if (isLoading) {
ReaderLoadingPlaceholder() ReaderLoadingPlaceholder()
} }

View file

@ -17,6 +17,12 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.settings.components.SettingsSubcategory import ua.acclorite.book_story.ui.settings.components.SettingsSubcategory
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HighlightedReadingOption import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HighlightedReadingOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HighlightedReadingThicknessOption import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HighlightedReadingThicknessOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HorizontalLimiterDimmingOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HorizontalLimiterHeightOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HorizontalLimiterOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HorizontalLimiterRulerOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HorizontalLimiterRulerThicknessOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.HorizontalLimiterVerticalOffsetOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.PerceptionExpanderOption import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.PerceptionExpanderOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.PerceptionExpanderPaddingOption import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.PerceptionExpanderPaddingOption
import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.PerceptionExpanderThicknessOption import ua.acclorite.book_story.ui.settings.reader.reading_speed.components.PerceptionExpanderThicknessOption
@ -52,5 +58,29 @@ fun LazyListScope.ReadingSpeedSubcategory(
item { item {
PerceptionExpanderThicknessOption() PerceptionExpanderThicknessOption()
} }
item {
HorizontalLimiterOption()
}
item {
HorizontalLimiterHeightOption()
}
item {
HorizontalLimiterVerticalOffsetOption()
}
item {
HorizontalLimiterRulerOption()
}
item {
HorizontalLimiterRulerThicknessOption()
}
item {
HorizontalLimiterDimmingOption()
}
} }
} }

View file

@ -0,0 +1,30 @@
/*
* 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.settings.reader.reading_speed.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.common.components.settings.SliderWithTitle
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
import ua.acclorite.book_story.ui.theme.ExpandingTransition
@Composable
fun HorizontalLimiterDimmingOption() {
val settings = LocalSettings.current
ExpandingTransition(visible = settings.horizontalLimiter.value) {
SliderWithTitle(
value = settings.horizontalLimiterDimming.value to "%",
toValue = 100,
title = stringResource(id = R.string.horizontal_limiter_dimming_option),
onValueChange = {
settings.horizontalLimiterDimming.update(it)
}
)
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.settings.reader.reading_speed.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.common.components.settings.SliderWithTitle
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
import ua.acclorite.book_story.ui.theme.ExpandingTransition
@Composable
fun HorizontalLimiterHeightOption() {
val settings = LocalSettings.current
ExpandingTransition(visible = settings.horizontalLimiter.value) {
SliderWithTitle(
value = settings.horizontalLimiterHeight.value to "pt",
fromValue = 1,
toValue = 24,
title = stringResource(id = R.string.horizontal_limiter_height_option),
onValueChange = {
settings.horizontalLimiterHeight.update(it)
}
)
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.settings.reader.reading_speed.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.common.components.settings.SwitchWithTitle
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
@Composable
fun HorizontalLimiterOption() {
val settings = LocalSettings.current
SwitchWithTitle(
selected = settings.horizontalLimiter.value,
title = stringResource(id = R.string.horizontal_limiter_option),
description = stringResource(id = R.string.horizontal_limiter_option_desc),
onClick = {
settings.horizontalLimiter.update(!settings.horizontalLimiter.lastValue)
}
)
}

View file

@ -0,0 +1,29 @@
/*
* 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.settings.reader.reading_speed.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.common.components.settings.SwitchWithTitle
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
import ua.acclorite.book_story.ui.theme.ExpandingTransition
@Composable
fun HorizontalLimiterRulerOption() {
val settings = LocalSettings.current
ExpandingTransition(visible = settings.horizontalLimiter.value) {
SwitchWithTitle(
selected = settings.horizontalLimiterRuler.value,
title = stringResource(id = R.string.horizontal_limiter_ruler_option),
onClick = {
settings.horizontalLimiterRuler.update(!settings.horizontalLimiterRuler.lastValue)
}
)
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.settings.reader.reading_speed.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.common.components.settings.SliderWithTitle
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
import ua.acclorite.book_story.ui.theme.ExpandingTransition
@Composable
fun HorizontalLimiterRulerThicknessOption() {
val settings = LocalSettings.current
ExpandingTransition(visible = settings.horizontalLimiter.value && settings.horizontalLimiterRuler.value) {
SliderWithTitle(
value = settings.horizontalLimiterRulerThickness.value to "pt",
fromValue = 1,
toValue = 12,
title = stringResource(id = R.string.horizontal_limiter_ruler_thickness_option),
onValueChange = {
settings.horizontalLimiterRulerThickness.update(it)
}
)
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.settings.reader.reading_speed.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.ui.common.components.settings.SliderWithTitle
import ua.acclorite.book_story.ui.common.helpers.LocalSettings
import ua.acclorite.book_story.ui.theme.ExpandingTransition
@Composable
fun HorizontalLimiterVerticalOffsetOption() {
val settings = LocalSettings.current
ExpandingTransition(visible = settings.horizontalLimiter.value) {
SliderWithTitle(
value = settings.horizontalLimiterVerticalOffset.value to "%",
toValue = 100,
title = stringResource(id = R.string.horizontal_limiter_vertical_offset_option),
onValueChange = {
settings.horizontalLimiterVerticalOffset.update(it)
}
)
}
}

View file

@ -194,6 +194,15 @@
<string name="perception_expander_option_desc">Збільшіть швидкість вашого читання фокусуючись на середині</string> <string name="perception_expander_option_desc">Збільшіть швидкість вашого читання фокусуючись на середині</string>
<string name="perception_expander_padding_option">Бокові відступи лінії</string> <string name="perception_expander_padding_option">Бокові відступи лінії</string>
<string name="perception_expander_thickness_option">Товщина лінії</string> <string name="perception_expander_thickness_option">Товщина лінії</string>
<string name="horizontal_limiter_option">Горизонтальний обмежувач</string>
<string name="horizontal_limiter_option_desc">
Показати горизонтальну лінійку та затемнити несфокусовану частину екрана
</string>
<string name="horizontal_limiter_height_option">Висота</string>
<string name="horizontal_limiter_vertical_offset_option">Вертикальне зміщення</string>
<string name="horizontal_limiter_ruler_option">Горизонтальна лінійка</string>
<string name="horizontal_limiter_ruler_thickness_option">Товщина лінійки</string>
<string name="horizontal_limiter_dimming_option">Затемнення</string>
<string name="screen_orientation_option">Орієнтація екрана</string> <string name="screen_orientation_option">Орієнтація екрана</string>
<string name="custom_screen_brightness_option">Користувацька яскравість</string> <string name="custom_screen_brightness_option">Користувацька яскравість</string>
<string name="screen_brightness_option">Яскравість</string> <string name="screen_brightness_option">Яскравість</string>

View file

@ -218,6 +218,15 @@
</string> </string>
<string name="perception_expander_padding_option">Line side margin</string> <string name="perception_expander_padding_option">Line side margin</string>
<string name="perception_expander_thickness_option">Line thickness</string> <string name="perception_expander_thickness_option">Line thickness</string>
<string name="horizontal_limiter_option">Horizontal limiter</string>
<string name="horizontal_limiter_option_desc">
Show horizontal ruler and dim unfocused part of the screen
</string>
<string name="horizontal_limiter_height_option">Height</string>
<string name="horizontal_limiter_vertical_offset_option">Vertical offset</string>
<string name="horizontal_limiter_ruler_option">Horizontal ruler</string>
<string name="horizontal_limiter_ruler_thickness_option">Ruler thickness</string>
<string name="horizontal_limiter_dimming_option">Dimming</string>
<string name="screen_orientation_option">Screen orientation</string> <string name="screen_orientation_option">Screen orientation</string>
<string name="custom_screen_brightness_option">Custom brightness</string> <string name="custom_screen_brightness_option">Custom brightness</string>
<string name="screen_brightness_option">Brightness</string> <string name="screen_brightness_option">Brightness</string>