From 1516b8fcba632a8c4977292493b0e25b9618c3e2 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:27:20 +0900 Subject: [PATCH 01/37] app: add verse long press gesture hook --- .../gnit/bible/app/ui/widgets/BibleReadingHelpers.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt index 0cce5710..ffe23f16 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt @@ -45,12 +45,14 @@ private fun normalBilingualUnderTranslationBackgroundColor( internal fun Modifier.verseTapGestures( verse: Int, onVerseTap: (Int) -> Unit, - onVerseDoubleTap: (Int) -> Unit + onVerseDoubleTap: (Int) -> Unit, + onVerseLongPress: (Int) -> Unit = {} ): Modifier { - return pointerInput(verse) { + return pointerInput(verse, onVerseTap, onVerseDoubleTap, onVerseLongPress) { detectTapGestures( onTap = { onVerseTap(verse) }, - onDoubleTap = { onVerseDoubleTap(verse) } + onDoubleTap = { onVerseDoubleTap(verse) }, + onLongPress = { onVerseLongPress(verse) } ) } } @@ -160,4 +162,4 @@ fun getVersePairs(bible: Bible, bibleState: BibleState): List Date: Wed, 8 Jul 2026 16:27:37 +0900 Subject: [PATCH 02/37] app: add Bible text selection model --- .../org/gnit/bible/app/BibleTextSelection.kt | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt new file mode 100644 index 00000000..2e61372e --- /dev/null +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt @@ -0,0 +1,108 @@ +package org.gnit.bible.app + +import org.gnit.bible.Translation +import org.gnit.bible.app.state.BibleState + +internal data class BibleTextSelection( + val firstUnit: Int, + val lastUnit: Int, + val unitCount: Int, + val unitsPerVerse: Int, + val anchorUnit: Int = firstUnit +) { + val anchorVerse: Int + get() { + if (unitCount <= 0) return 1 + return (anchorUnit.coerceIn(0, unitCount - 1) / unitsPerVerse) + 1 + } + + fun containsSingleVerse(verse: Int): Boolean { + if (unitsPerVerse != SINGLE_UNITS_PER_VERSE) return false + return (verse - 1) in selectedUnitRange() + } + + fun containsBilingualVersePart(verse: Int, isSubTranslation: Boolean): Boolean { + if (unitsPerVerse != BILINGUAL_UNITS_PER_VERSE) return false + val unit = ((verse - 1) * BILINGUAL_UNITS_PER_VERSE) + if (isSubTranslation) 1 else 0 + return unit in selectedUnitRange() + } + + fun selectAll(): BibleTextSelection { + if (unitCount <= 0) return this + return copy(firstUnit = 0, lastUnit = unitCount - 1) + } + + fun copySingleText(bibleState: BibleState, verses: Array): String { + val selectedVerses = selectedUnitRange() + .map { unit -> unit + 1 } + .filter { verse -> verse in 1..verses.size } + if (selectedVerses.isEmpty()) return "" + + return buildString { + appendLine("${bibleState.describeBookChapter()} ${bibleState.mainTranslation.code.uppercase()}") + selectedVerses.forEach { verse -> + appendLine("$verse ${verses[verse - 1]}") + } + }.trimEnd() + } + + fun copyBilingualText(bibleState: BibleState, versePairs: List>): String { + val subTranslation = bibleState.subTranslation ?: return "" + val selectedUnits = selectedUnitRange() + .filter { unit -> unit / BILINGUAL_UNITS_PER_VERSE in versePairs.indices } + if (selectedUnits.isEmpty()) return "" + + return buildString { + appendLine( + "${bibleState.describeBookChapter()} " + + "${bibleState.mainTranslation.code.uppercase()} / ${subTranslation.code.uppercase()}" + ) + selectedUnits.forEach { unit -> + val verseIndex = unit / BILINGUAL_UNITS_PER_VERSE + val isSubTranslation = unit % BILINGUAL_UNITS_PER_VERSE == 1 + val verseNumber = verseIndex + 1 + val translation = if (isSubTranslation) subTranslation else bibleState.mainTranslation + val verseText = if (isSubTranslation) versePairs[verseIndex].second else versePairs[verseIndex].first + appendLine("$verseNumber ${translation.copyLabel()} $verseText") + } + }.trimEnd() + } + + private fun selectedUnitRange(): IntRange { + if (unitCount <= 0) return IntRange.EMPTY + val first = minOf(firstUnit, lastUnit).coerceIn(0, unitCount - 1) + val last = maxOf(firstUnit, lastUnit).coerceIn(0, unitCount - 1) + return first..last + } + + private fun Translation.copyLabel(): String = "[${code.uppercase()}]" + + companion object { + private const val SINGLE_UNITS_PER_VERSE = 1 + private const val BILINGUAL_UNITS_PER_VERSE = 2 + + fun singleVerse(verse: Int, verseCount: Int): BibleTextSelection { + val unitCount = verseCount.coerceAtLeast(0) + val unit = (verse - 1).coerceIn(0, (unitCount - 1).coerceAtLeast(0)) + return BibleTextSelection( + firstUnit = unit, + lastUnit = unit, + unitCount = unitCount, + unitsPerVerse = SINGLE_UNITS_PER_VERSE, + anchorUnit = unit + ) + } + + fun bilingualMainVerse(verse: Int, verseCount: Int): BibleTextSelection { + val unitCount = (verseCount * BILINGUAL_UNITS_PER_VERSE).coerceAtLeast(0) + val unit = ((verse - 1) * BILINGUAL_UNITS_PER_VERSE).coerceIn(0, (unitCount - 1).coerceAtLeast(0)) + return BibleTextSelection( + firstUnit = unit, + lastUnit = unit, + unitCount = unitCount, + unitsPerVerse = BILINGUAL_UNITS_PER_VERSE, + anchorUnit = unit + ) + } + } +} From 797bb7863afd0e20f5d3e6328648b93df1b16fdd Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:27:52 +0900 Subject: [PATCH 03/37] app: highlight selected single Bible verses --- .../org/gnit/bible/app/ui/widgets/SingleBible.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt index 8b11bf74..c3ce472e 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -16,6 +17,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.gnit.bible.app.BibleTextSelection import org.gnit.bible.app.ScrollableColumn import org.gnit.bible.app.VerseLayoutInfo import org.gnit.bible.app.state.BibleState @@ -30,8 +32,10 @@ fun SingleBible( onScrollPercentChange: (Float) -> Unit = {}, onVersePositioned: (Int, VerseLayoutInfo) -> Unit = { _, _ -> }, highlightedVerse: Int? = null, + selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, + onVerseLongPress: (Int) -> Unit = {}, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -47,8 +51,11 @@ fun SingleBible( onTitleTap = onTitleTap ) { verses.forEachIndexed { verse, text -> - val background = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value - val textColor = animatedVerseTextColor(verse, highlightedVerse).value + val animatedBackground = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value + val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value + val isSelected = selectedTextSelection?.containsSingleVerse(verse + 1) == true + val background = if (isSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground + val textColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor Column( modifier = Modifier @@ -56,7 +63,8 @@ fun SingleBible( .verseTapGestures( verse = verse + 1, onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap + onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress ) .onGloballyPositioned { coordinates -> onVersePositioned( @@ -100,4 +108,4 @@ fun SingleBiblePreview() { scrollState = scrollState ) } -} \ No newline at end of file +} From f641f9ba5d7877e108e26af8c66caa337cbc7f66 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:28:08 +0900 Subject: [PATCH 04/37] app: highlight selected side-by-side verses --- .../app/ui/widgets/BilingualSideBible.kt | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt index f46c1fab..737e0817 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -19,6 +20,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.gnit.bible.app.BibleTextSelection import org.gnit.bible.app.ScrollableColumn import org.gnit.bible.app.VerseLayoutInfo import org.gnit.bible.app.state.BibleState @@ -34,8 +36,10 @@ fun BilingualSideBible( onScrollPercentChange: (Float) -> Unit = {}, onVersePositioned: (Int, VerseLayoutInfo) -> Unit = { _, _ -> }, highlightedVerse: Int? = null, + selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, + onVerseLongPress: (Int) -> Unit = {}, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -53,20 +57,34 @@ fun BilingualSideBible( onTitleTap = onTitleTap ) { versePairs.forEachIndexed { verse, pair -> - val background = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value - val textColor = animatedVerseTextColor(verse, highlightedVerse).value + val animatedBackground = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value + val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value + val verseNumber = verse + 1 + val isMainSelected = selectedTextSelection?.containsBilingualVersePart( + verse = verseNumber, + isSubTranslation = false + ) == true + val isSubSelected = selectedTextSelection?.containsBilingualVersePart( + verse = verseNumber, + isSubTranslation = true + ) == true + val mainBackground = if (isMainSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground + val subBackground = if (isSubSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground + val mainTextColor = if (isMainSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor + val subTextColor = if (isSubSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor Column( modifier = Modifier .fillMaxWidth() .verseTapGestures( - verse = verse + 1, + verse = verseNumber, onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap + onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress ) .onGloballyPositioned { coordinates -> onVersePositioned( - verse + 1, + verseNumber, VerseLayoutInfo( topPx = coordinates.positionInParent().y.toInt(), heightPx = coordinates.size.height @@ -75,27 +93,29 @@ fun BilingualSideBible( } ) { Row( - modifier = Modifier - .fillMaxWidth() - .background(background) + modifier = Modifier.fillMaxWidth() ) { Text( - text = "${verse + 1} ${pair.first}", + text = "$verseNumber ${pair.first}", style = TextStyle( fontSize = bibleState.fontSize.sp, fontFamily = if (bibleState.isFontFamilySerif) bibleState.mainTranslation.language.serifFontFamily() else bibleState.mainTranslation.language.sansFontFamily(), - color = textColor + color = mainTextColor ), - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .background(mainBackground) ) Text( - text = "${verse + 1} ${pair.second}", + text = "$verseNumber ${pair.second}", style = TextStyle( fontSize = bibleState.fontSize.sp, fontFamily = if (bibleState.isFontFamilySerif) bibleState.subTranslation.language.serifFontFamily() else bibleState.subTranslation.language.sansFontFamily(), - color = textColor + color = subTextColor ), - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .background(subBackground) ) } Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) @@ -121,4 +141,4 @@ fun BilingualSideBiblePreview() { scrollState = scrollState ) } -} \ No newline at end of file +} From 49f91f27cc15763fa8646080fec4583e6da1a13a Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:28:24 +0900 Subject: [PATCH 05/37] app: highlight selected bilingual under verses --- .../app/ui/widgets/BilingualUnderBible.kt | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt index ac8642d6..3dc53e80 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,6 +19,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import org.gnit.bible.app.BibleTextSelection import org.gnit.bible.app.ScrollableColumn import org.gnit.bible.app.VerseLayoutInfo import org.gnit.bible.app.state.BibleState @@ -33,8 +35,10 @@ fun BilingualUnderBible( onScrollPercentChange: (Float) -> Unit = {}, onVersePositioned: (Int, VerseLayoutInfo) -> Unit = { _, _ -> }, highlightedVerse: Int? = null, + selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, + onVerseLongPress: (Int) -> Unit = {}, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -52,31 +56,45 @@ fun BilingualUnderBible( onTitleTap = onTitleTap ) { versePairs.forEachIndexed { verse, pair -> - val mainBackground = animatedBilingualUnderTranslationBackgroundColor( + val animatedMainBackground = animatedBilingualUnderTranslationBackgroundColor( bibleState = bibleState, verseIndex = verse, highlightedVerse = highlightedVerse, isSubTranslation = false ).value - val subBackground = animatedBilingualUnderTranslationBackgroundColor( + val animatedSubBackground = animatedBilingualUnderTranslationBackgroundColor( bibleState = bibleState, verseIndex = verse, highlightedVerse = highlightedVerse, isSubTranslation = true ).value - val textColor = animatedVerseTextColor(verse, highlightedVerse).value + val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value + val verseNumber = verse + 1 + val isMainSelected = selectedTextSelection?.containsBilingualVersePart( + verse = verseNumber, + isSubTranslation = false + ) == true + val isSubSelected = selectedTextSelection?.containsBilingualVersePart( + verse = verseNumber, + isSubTranslation = true + ) == true + val mainBackground = if (isMainSelected) MaterialTheme.colorScheme.primaryContainer else animatedMainBackground + val subBackground = if (isSubSelected) MaterialTheme.colorScheme.primaryContainer else animatedSubBackground + val mainTextColor = if (isMainSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor + val subTextColor = if (isSubSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor Column( modifier = Modifier .fillMaxWidth() .verseTapGestures( - verse = verse + 1, + verse = verseNumber, onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap + onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress ) .onGloballyPositioned { coordinates -> onVersePositioned( - verse + 1, + verseNumber, VerseLayoutInfo( topPx = coordinates.positionInParent().y.toInt(), heightPx = coordinates.size.height @@ -85,22 +103,22 @@ fun BilingualUnderBible( } ) { Text( - text = "${verse + 1} ${pair.first}", + text = "$verseNumber ${pair.first}", style = TextStyle( fontSize = bibleState.fontSize.sp, fontFamily = if (bibleState.isFontFamilySerif) bibleState.mainTranslation.language.serifFontFamily() else bibleState.mainTranslation.language.sansFontFamily(), - color = textColor + color = mainTextColor ), modifier = Modifier .fillMaxWidth() .background(mainBackground) ) Text( - text = "${verse + 1} ${pair.second}", + text = "$verseNumber ${pair.second}", style = TextStyle( fontSize = bibleState.fontSize.sp, fontFamily = if (bibleState.isFontFamilySerif) bibleState.subTranslation.language.serifFontFamily() else bibleState.subTranslation.language.sansFontFamily(), - color = textColor + color = subTextColor ), modifier = Modifier .fillMaxWidth() @@ -129,4 +147,4 @@ fun BilingualUnderBiblePreview() { scrollState = scrollState ) } -} \ No newline at end of file +} From 52acb4e07c4adcb9edad31aefa2b280f227e7340 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:29:23 +0900 Subject: [PATCH 06/37] app: show icon-only copy popup on verse long press --- .../gnit/bible/app/screens/ReadingScreen.kt | 142 +++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt index 28ca8b90..232dd847 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt @@ -8,17 +8,22 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -38,10 +43,14 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.isCtrlPressed import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.Dispatchers @@ -61,6 +70,7 @@ import org.gnit.bible.app.ui.widgets.BilingualUnderBible import org.gnit.bible.app.ui.widgets.SingleBible import org.gnit.bible.app.ui.widgets.sansFontFamily import org.gnit.bible.app.ui.widgets.serifFontFamily +import org.jetbrains.compose.resources.painterResource import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt @@ -105,6 +115,16 @@ fun BibleReadingArea( mutableStateMapOf() } var viewportHeight by remember { mutableStateOf(0) } + var selectedTextSelection by remember( + state.book, + state.chapter, + state.readingMode, + state.mainTranslation, + state.subTranslation + ) { + mutableStateOf(null) + } + val clipboardManager = LocalClipboardManager.current LaunchedEffect(scrollState) { snapshotFlow { scrollState.isScrollInProgress }.collect { inProgress -> @@ -190,12 +210,15 @@ fun BibleReadingArea( onStateChange(currentState.copy(scrollPercent = scrollPercent)) } val onTitleTap: () -> Unit = { + selectedTextSelection = null if (!currentState.isSearchActive) { onSearchRequested() } } val onVerseTap: (Int) -> Unit = { - if (currentState.isSearchActive) { + if (selectedTextSelection != null) { + selectedTextSelection = null + } else if (currentState.isSearchActive) { onSearchCancel() } else if (chrome.isVisible()) { chrome.forceHide() @@ -204,9 +227,50 @@ fun BibleReadingArea( } } val onVerseDoubleTap: (Int) -> Unit = { verse -> + selectedTextSelection = null onStateChange(currentState.recordReadHistory(verse)) chrome.onUserInteraction() } + val onVerseLongPress: (Int) -> Unit = { verse -> + selectedTextSelection = when (val content = readingContent) { + is ReadingContent.Single -> BibleTextSelection.singleVerse( + verse = verse, + verseCount = content.verses.size + ) + + is ReadingContent.Bilingual -> BibleTextSelection.bilingualMainVerse( + verse = verse, + verseCount = content.versePairs.size + ) + + null -> null + } + if (selectedTextSelection != null) { + chrome.forceHide() + } + } + val copySelectedText: () -> Unit = { + val selection = selectedTextSelection + if (selection != null) { + val selectedText = when (val content = readingContent) { + is ReadingContent.Single -> selection.copySingleText( + bibleState = currentState, + verses = content.verses + ) + + is ReadingContent.Bilingual -> selection.copyBilingualText( + bibleState = currentState, + versePairs = content.versePairs + ) + + null -> "" + } + if (selectedText.isNotBlank()) { + clipboardManager.setText(AnnotatedString(selectedText)) + } + } + selectedTextSelection = null + } Box( modifier = modifier @@ -237,8 +301,10 @@ fun BibleReadingArea( onScrollPercentChange = onScrollPercentChange, onVersePositioned = { verse, layout -> verseLayouts[verse] = layout }, highlightedVerse = state.highlightedVerse, + selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress, topContentPadding = topContentPadding, bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap @@ -252,8 +318,10 @@ fun BibleReadingArea( onScrollPercentChange = onScrollPercentChange, onVersePositioned = { verse, layout -> verseLayouts[verse] = layout }, highlightedVerse = state.highlightedVerse, + selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress, topContentPadding = topContentPadding, bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap @@ -266,8 +334,10 @@ fun BibleReadingArea( onScrollPercentChange = onScrollPercentChange, onVersePositioned = { verse, layout -> verseLayouts[verse] = layout }, highlightedVerse = state.highlightedVerse, + selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress, topContentPadding = topContentPadding, bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap @@ -276,6 +346,16 @@ fun BibleReadingArea( ReadingMode.SINGLE -> Unit } } + + selectedTextSelection?.let { selection -> + BibleTextSelectionPopup( + selectedVerseLayout = verseLayouts[selection.anchorVerse], + scrollState = scrollState, + viewportHeight = viewportHeight, + onCopy = copySelectedText, + onSelectAll = { selectedTextSelection = selection.selectAll() } + ) + } } LaunchedEffect(state.highlightedVerse) { @@ -300,6 +380,64 @@ fun BibleReadingArea( } } +@Composable +private fun BibleTextSelectionPopup( + selectedVerseLayout: VerseLayoutInfo?, + scrollState: ScrollState, + viewportHeight: Int, + onCopy: () -> Unit, + onSelectAll: () -> Unit +) { + if (selectedVerseLayout == null || viewportHeight <= 0) return + + val density = LocalDensity.current + val popupHeightPx = with(density) { 52.dp.roundToPx() } + val gapPx = with(density) { 8.dp.roundToPx() } + val verseTopInViewport = selectedVerseLayout.topPx - scrollState.value + val shouldShowAbove = verseTopInViewport > viewportHeight / 2 + val rawPopupY = if (shouldShowAbove) { + verseTopInViewport - popupHeightPx - gapPx + } else { + verseTopInViewport + selectedVerseLayout.heightPx + gapPx + } + val maxPopupY = (viewportHeight - popupHeightPx).coerceAtLeast(0) + val popupY = rawPopupY.coerceIn(0, maxPopupY) + + Box( + modifier = Modifier + .fillMaxWidth() + .offset { IntOffset(0, popupY) }, + contentAlignment = Alignment.Center + ) { + Surface( + shape = RoundedCornerShape(28.dp), + tonalElevation = 6.dp, + shadowElevation = 6.dp, + color = MaterialTheme.colorScheme.surfaceVariant + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp) + ) { + IconButton(onClick = onCopy) { + Icon( + painter = painterResource(Res.drawable.content_copy), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + IconButton(onClick = onSelectAll) { + Icon( + painter = painterResource(Res.drawable.select_all), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + private sealed interface ReadingContent { data class Single(val verses: Array) : ReadingContent data class Bilingual(val versePairs: List>) : ReadingContent From bfbe46bc7c7924bde01f29b10c854002e65986d2 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:42:05 +0900 Subject: [PATCH 07/37] app: expose Bible text selection type for public widgets --- .../commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt index 2e61372e..be773d41 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt @@ -3,7 +3,7 @@ package org.gnit.bible.app import org.gnit.bible.Translation import org.gnit.bible.app.state.BibleState -internal data class BibleTextSelection( +data class BibleTextSelection( val firstUnit: Int, val lastUnit: Int, val unitCount: Int, From 4d983f31100b29cdf9fb2398bac0c7310d46252e Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:52:51 +0900 Subject: [PATCH 08/37] app: make copy icon tintable in compose resources --- .../commonMain/composeResources/drawable/content_copy.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/shared/src/commonMain/composeResources/drawable/content_copy.xml b/app/shared/src/commonMain/composeResources/drawable/content_copy.xml index c744e1aa..1285b04e 100644 --- a/app/shared/src/commonMain/composeResources/drawable/content_copy.xml +++ b/app/shared/src/commonMain/composeResources/drawable/content_copy.xml @@ -2,9 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="960" - android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="960"> From 1bf7fa736f163fd098259e9d6f90f23a233186b8 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:53:01 +0900 Subject: [PATCH 09/37] app: make select all icon tintable in compose resources --- .../src/commonMain/composeResources/drawable/select_all.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/shared/src/commonMain/composeResources/drawable/select_all.xml b/app/shared/src/commonMain/composeResources/drawable/select_all.xml index 17d20a75..5a850039 100644 --- a/app/shared/src/commonMain/composeResources/drawable/select_all.xml +++ b/app/shared/src/commonMain/composeResources/drawable/select_all.xml @@ -2,9 +2,8 @@ android:width="24dp" android:height="24dp" android:viewportWidth="960" - android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="960"> From 207399da2ec643edeea8ad41bbf1d164c0d2dcad Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:54:30 +0900 Subject: [PATCH 10/37] app: align icon resource style --- .../commonMain/composeResources/drawable/content_copy.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/shared/src/commonMain/composeResources/drawable/content_copy.xml b/app/shared/src/commonMain/composeResources/drawable/content_copy.xml index 1285b04e..9680dbdb 100644 --- a/app/shared/src/commonMain/composeResources/drawable/content_copy.xml +++ b/app/shared/src/commonMain/composeResources/drawable/content_copy.xml @@ -1,9 +1,9 @@ From 85fbb06dbae22daa4c98b33a05cadc3806809481 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:54:48 +0900 Subject: [PATCH 11/37] app: align select all icon resource style --- .../src/commonMain/composeResources/drawable/select_all.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/shared/src/commonMain/composeResources/drawable/select_all.xml b/app/shared/src/commonMain/composeResources/drawable/select_all.xml index 5a850039..b80727d9 100644 --- a/app/shared/src/commonMain/composeResources/drawable/select_all.xml +++ b/app/shared/src/commonMain/composeResources/drawable/select_all.xml @@ -1,9 +1,9 @@ From 3f0b0cd63491d88750dfd3e84349ef731de1eba4 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 16:55:48 +0900 Subject: [PATCH 12/37] app: tint selection popup icons with primary color --- .../kotlin/org/gnit/bible/app/screens/ReadingScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt index 232dd847..46e1e520 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt @@ -423,14 +423,14 @@ private fun BibleTextSelectionPopup( Icon( painter = painterResource(Res.drawable.content_copy), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.primary ) } IconButton(onClick = onSelectAll) { Icon( painter = painterResource(Res.drawable.select_all), contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.primary ) } } From 61ef6bd771343aefcfdd6194ecdb29ad70f4c205 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:17:27 +0900 Subject: [PATCH 13/37] smaller select/copy icon --- .../composeResources/drawable/content_copy.xml | 4 ++-- .../composeResources/drawable/select_all.xml | 4 ++-- .../org/gnit/bible/app/screens/ReadingScreen.kt | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/shared/src/commonMain/composeResources/drawable/content_copy.xml b/app/shared/src/commonMain/composeResources/drawable/content_copy.xml index 9680dbdb..492c0e6c 100644 --- a/app/shared/src/commonMain/composeResources/drawable/content_copy.xml +++ b/app/shared/src/commonMain/composeResources/drawable/content_copy.xml @@ -1,6 +1,6 @@ viewportHeight / 2 val rawPopupY = if (shouldShowAbove) { @@ -410,7 +410,7 @@ private fun BibleTextSelectionPopup( contentAlignment = Alignment.Center ) { Surface( - shape = RoundedCornerShape(28.dp), + shape = RoundedCornerShape(10.dp), tonalElevation = 6.dp, shadowElevation = 6.dp, color = MaterialTheme.colorScheme.surfaceVariant @@ -422,15 +422,15 @@ private fun BibleTextSelectionPopup( IconButton(onClick = onCopy) { Icon( painter = painterResource(Res.drawable.content_copy), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary + contentDescription = null/*, + tint = MaterialTheme.colorScheme.primary*/ ) } IconButton(onClick = onSelectAll) { Icon( painter = painterResource(Res.drawable.select_all), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary + contentDescription = null/*, + tint = MaterialTheme.colorScheme.primary*/ ) } } From 531a5b11a0124b02f1f8e6df70c87eb4295abdb7 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:22:17 +0900 Subject: [PATCH 14/37] app: position selection popup from text bounds --- .../kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt index c3ce472e..c203a9c1 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -41,6 +42,7 @@ fun SingleBible( onTitleTap: () -> Unit = {} ) { val translation = bibleState.mainTranslation + val verseSpacingPx = with(LocalDensity.current) { bibleState.spaceBetweenVerses.dp.roundToPx() } ScrollableColumn( bibleState = bibleState, @@ -71,7 +73,7 @@ fun SingleBible( verse + 1, VerseLayoutInfo( topPx = coordinates.positionInParent().y.toInt(), - heightPx = coordinates.size.height + heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) ) ) } From 47b8e60c9d2bf22284131f772e6d813a87c86855 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:22:33 +0900 Subject: [PATCH 15/37] app: trim bilingual side verse popup bounds --- .../org/gnit/bible/app/ui/widgets/BilingualSideBible.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt index 737e0817..1a295b88 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -47,6 +48,7 @@ fun BilingualSideBible( val readingMode = bibleState.readingMode require(readingMode == ReadingMode.BILINGUAL_SIDE) { "ReadingMode should be ${ReadingMode.BILINGUAL_SIDE} but trying to put $readingMode" } requireNotNull(bibleState.subTranslation) { "ReadingMode should be ${ReadingMode.BILINGUAL_SIDE} so subTranslation is needed but null" } + val verseSpacingPx = with(LocalDensity.current) { bibleState.spaceBetweenVerses.dp.roundToPx() } ScrollableColumn( bibleState = bibleState, @@ -87,7 +89,7 @@ fun BilingualSideBible( verseNumber, VerseLayoutInfo( topPx = coordinates.positionInParent().y.toInt(), - heightPx = coordinates.size.height + heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) ) ) } From 9eb0f346d9d7f2c06651a7af490f1b87ecef9162 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:26:05 +0900 Subject: [PATCH 16/37] app: trim bilingual under verse popup bounds --- .../org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt index 3dc53e80..f1eefd8d 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -46,6 +47,7 @@ fun BilingualUnderBible( val readingMode = bibleState.readingMode require(readingMode == ReadingMode.BILINGUAL_UNDER) { "Expected ${ReadingMode.BILINGUAL_UNDER}, got $readingMode" } requireNotNull(bibleState.subTranslation) { "subTranslation is required for ${ReadingMode.BILINGUAL_UNDER}" } + val verseSpacingPx = with(LocalDensity.current) { bibleState.spaceBetweenVerses.dp.roundToPx() } ScrollableColumn( bibleState = bibleState, @@ -97,7 +99,7 @@ fun BilingualUnderBible( verseNumber, VerseLayoutInfo( topPx = coordinates.positionInParent().y.toInt(), - heightPx = coordinates.size.height + heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) ) ) } From ed03ee18dca94c8885ee3ebca37ee1eb3b4319e1 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:27:48 +0900 Subject: [PATCH 17/37] app: derive selection popup position from static size constants --- .../gnit/bible/app/screens/ReadingScreen.kt | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt index 3de399dd..245c2daf 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt @@ -16,12 +16,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -41,8 +41,8 @@ import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.isCtrlPressed -import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode @@ -83,6 +83,10 @@ const val READING_SCREEN_VERTICAL_SPACE = 1 const val BOOK_CONTROLS_BAR_BOTTOM_MARGIN = READING_SCREEN_VERTICAL_SPACE const val CHAPTER_CONTROLS_BAR_TOP_MARGIN = READING_SCREEN_VERTICAL_SPACE const val READING_SCREEN_HORIZONTAL_PADDING = 4 +const val SELECTION_POPUP_ICON_SIZE: Int = 24 +const val SELECTION_POPUP_PADDING: Int = 2 +const val SELECTION_POPUP_GAP: Int = 1 +private const val SELECTION_POPUP_HEIGHT: Int = SELECTION_POPUP_ICON_SIZE + (SELECTION_POPUP_PADDING * 2) @Composable fun BibleReadingArea( @@ -391,8 +395,8 @@ private fun BibleTextSelectionPopup( if (selectedVerseLayout == null || viewportHeight <= 0) return val density = LocalDensity.current - val popupHeightPx = with(density) { 52.dp.roundToPx() } - val gapPx = with(density) { 1.dp.roundToPx() } + val popupHeightPx = with(density) { SELECTION_POPUP_HEIGHT.dp.roundToPx() } + val gapPx = with(density) { SELECTION_POPUP_GAP.dp.roundToPx() } val verseTopInViewport = selectedVerseLayout.topPx - scrollState.value val shouldShowAbove = verseTopInViewport > viewportHeight / 2 val rawPopupY = if (shouldShowAbove) { @@ -416,21 +420,20 @@ private fun BibleTextSelectionPopup( color = MaterialTheme.colorScheme.surfaceVariant ) { Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp) + verticalAlignment = Alignment.CenterVertically ) { - IconButton(onClick = onCopy) { + SelectionPopupIconButton(onClick = onCopy) { Icon( painter = painterResource(Res.drawable.content_copy), - contentDescription = null/*, - tint = MaterialTheme.colorScheme.primary*/ + contentDescription = null, + modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp) ) } - IconButton(onClick = onSelectAll) { + SelectionPopupIconButton(onClick = onSelectAll) { Icon( painter = painterResource(Res.drawable.select_all), - contentDescription = null/*, - tint = MaterialTheme.colorScheme.primary*/ + contentDescription = null, + modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp) ) } } @@ -438,6 +441,21 @@ private fun BibleTextSelectionPopup( } } +@Composable +private fun SelectionPopupIconButton( + onClick: () -> Unit, + content: @Composable () -> Unit +) { + Box( + modifier = Modifier + .clickable(onClick = onClick) + .padding(SELECTION_POPUP_PADDING.dp), + contentAlignment = Alignment.Center + ) { + content() + } +} + private sealed interface ReadingContent { data class Single(val verses: Array) : ReadingContent data class Bilingual(val versePairs: List>) : ReadingContent From aa6c2efdbab8e240d19419077ceaff2e863c095f Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:37:37 +0900 Subject: [PATCH 18/37] compact but easy to tap select/copy icon --- .../kotlin/org/gnit/bible/app/screens/ReadingScreen.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt index 245c2daf..993278af 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt @@ -83,9 +83,9 @@ const val READING_SCREEN_VERTICAL_SPACE = 1 const val BOOK_CONTROLS_BAR_BOTTOM_MARGIN = READING_SCREEN_VERTICAL_SPACE const val CHAPTER_CONTROLS_BAR_TOP_MARGIN = READING_SCREEN_VERTICAL_SPACE const val READING_SCREEN_HORIZONTAL_PADDING = 4 -const val SELECTION_POPUP_ICON_SIZE: Int = 24 -const val SELECTION_POPUP_PADDING: Int = 2 -const val SELECTION_POPUP_GAP: Int = 1 +const val SELECTION_POPUP_ICON_SIZE: Int = 20 +const val SELECTION_POPUP_PADDING: Int = 6 +const val SELECTION_POPUP_GAP: Int = 0 private const val SELECTION_POPUP_HEIGHT: Int = SELECTION_POPUP_ICON_SIZE + (SELECTION_POPUP_PADDING * 2) @Composable @@ -422,6 +422,7 @@ private fun BibleTextSelectionPopup( Row( verticalAlignment = Alignment.CenterVertically ) { + Spacer(modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp)) SelectionPopupIconButton(onClick = onCopy) { Icon( painter = painterResource(Res.drawable.content_copy), @@ -429,6 +430,7 @@ private fun BibleTextSelectionPopup( modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp) ) } + Spacer(modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp)) SelectionPopupIconButton(onClick = onSelectAll) { Icon( painter = painterResource(Res.drawable.select_all), @@ -436,6 +438,7 @@ private fun BibleTextSelectionPopup( modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp) ) } + Spacer(modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp)) } } } From 72712521b2a4b9ebd049312987a66248cd34ee80 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:44:32 +0900 Subject: [PATCH 19/37] copied text should not include translation code --- .../kotlin/org/gnit/bible/app/BibleTextSelection.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt index be773d41..495c7d1e 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt @@ -39,7 +39,7 @@ data class BibleTextSelection( if (selectedVerses.isEmpty()) return "" return buildString { - appendLine("${bibleState.describeBookChapter()} ${bibleState.mainTranslation.code.uppercase()}") + appendLine(bibleState.describeBookChapter()) selectedVerses.forEach { verse -> appendLine("$verse ${verses[verse - 1]}") } @@ -53,10 +53,7 @@ data class BibleTextSelection( if (selectedUnits.isEmpty()) return "" return buildString { - appendLine( - "${bibleState.describeBookChapter()} " + - "${bibleState.mainTranslation.code.uppercase()} / ${subTranslation.code.uppercase()}" - ) + appendLine(bibleState.describeBookChapter()) selectedUnits.forEach { unit -> val verseIndex = unit / BILINGUAL_UNITS_PER_VERSE val isSubTranslation = unit % BILINGUAL_UNITS_PER_VERSE == 1 From 8cd47a06fc21f68c9a6247f801b9ca088f70e51c Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:55:23 +0900 Subject: [PATCH 20/37] app: let platform text selection handle long press --- .../org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt index ffe23f16..ed61b3b9 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt @@ -46,13 +46,15 @@ internal fun Modifier.verseTapGestures( verse: Int, onVerseTap: (Int) -> Unit, onVerseDoubleTap: (Int) -> Unit, - onVerseLongPress: (Int) -> Unit = {} + onVerseLongPress: ((Int) -> Unit)? = null ): Modifier { return pointerInput(verse, onVerseTap, onVerseDoubleTap, onVerseLongPress) { detectTapGestures( onTap = { onVerseTap(verse) }, onDoubleTap = { onVerseDoubleTap(verse) }, - onLongPress = { onVerseLongPress(verse) } + onLongPress = onVerseLongPress?.let { longPress -> + { longPress(verse) } + } ) } } From 9656d8d489b2471d2c7e4fdc4ba4e60d58288916 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:55:37 +0900 Subject: [PATCH 21/37] app: enable platform text selection in single reader --- .../gnit/bible/app/ui/widgets/SingleBible.kt | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt index c203a9c1..1b5d99b3 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -36,7 +37,7 @@ fun SingleBible( selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, - onVerseLongPress: (Int) -> Unit = {}, + onVerseLongPress: ((Int) -> Unit)? = null, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -52,48 +53,52 @@ fun SingleBible( bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap ) { - verses.forEachIndexed { verse, text -> - val animatedBackground = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value - val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value - val isSelected = selectedTextSelection?.containsSingleVerse(verse + 1) == true - val background = if (isSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground - val textColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor + SelectionContainer { + Column { + verses.forEachIndexed { verse, text -> + val animatedBackground = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value + val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value + val isSelected = selectedTextSelection?.containsSingleVerse(verse + 1) == true + val background = if (isSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground + val textColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor - Column( - modifier = Modifier - .fillMaxWidth() - .verseTapGestures( - verse = verse + 1, - onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress - ) - .onGloballyPositioned { coordinates -> - onVersePositioned( - verse + 1, - VerseLayoutInfo( - topPx = coordinates.positionInParent().y.toInt(), - heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) + Column( + modifier = Modifier + .fillMaxWidth() + .verseTapGestures( + verse = verse + 1, + onVerseTap = onVerseTap, + onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress ) + .onGloballyPositioned { coordinates -> + onVersePositioned( + verse + 1, + VerseLayoutInfo( + topPx = coordinates.positionInParent().y.toInt(), + heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) + ) + ) + } + ) { + Text( + text = "${verse + 1} $text", + style = TextStyle( + fontSize = bibleState.fontSize.sp, + fontFamily = if (bibleState.isFontFamilySerif) { + translation.language.serifFontFamily() + } else { + translation.language.sansFontFamily() + }, + color = textColor + ), + modifier = Modifier + .fillMaxWidth() + .background(background) ) + Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) } - ) { - Text( - text = "${verse + 1} $text", - style = TextStyle( - fontSize = bibleState.fontSize.sp, - fontFamily = if (bibleState.isFontFamilySerif) { - translation.language.serifFontFamily() - } else { - translation.language.sansFontFamily() - }, - color = textColor - ), - modifier = Modifier - .fillMaxWidth() - .background(background) - ) - Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) + } } } } From 28168914fdad11668899ce0497061d85cce56f5a Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:56:00 +0900 Subject: [PATCH 22/37] app: enable platform text selection in side reader --- .../app/ui/widgets/BilingualSideBible.kt | 125 +++++++++--------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt index 1a295b88..b93636cf 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,7 +41,7 @@ fun BilingualSideBible( selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, - onVerseLongPress: (Int) -> Unit = {}, + onVerseLongPress: ((Int) -> Unit)? = null, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -58,69 +59,73 @@ fun BilingualSideBible( bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap ) { - versePairs.forEachIndexed { verse, pair -> - val animatedBackground = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value - val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value - val verseNumber = verse + 1 - val isMainSelected = selectedTextSelection?.containsBilingualVersePart( - verse = verseNumber, - isSubTranslation = false - ) == true - val isSubSelected = selectedTextSelection?.containsBilingualVersePart( - verse = verseNumber, - isSubTranslation = true - ) == true - val mainBackground = if (isMainSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground - val subBackground = if (isSubSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground - val mainTextColor = if (isMainSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor - val subTextColor = if (isSubSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor - - Column( - modifier = Modifier - .fillMaxWidth() - .verseTapGestures( + SelectionContainer { + Column { + versePairs.forEachIndexed { verse, pair -> + val animatedBackground = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value + val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value + val verseNumber = verse + 1 + val isMainSelected = selectedTextSelection?.containsBilingualVersePart( + verse = verseNumber, + isSubTranslation = false + ) == true + val isSubSelected = selectedTextSelection?.containsBilingualVersePart( verse = verseNumber, - onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress - ) - .onGloballyPositioned { coordinates -> - onVersePositioned( - verseNumber, - VerseLayoutInfo( - topPx = coordinates.positionInParent().y.toInt(), - heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) + isSubTranslation = true + ) == true + val mainBackground = if (isMainSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground + val subBackground = if (isSubSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground + val mainTextColor = if (isMainSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor + val subTextColor = if (isSubSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor + + Column( + modifier = Modifier + .fillMaxWidth() + .verseTapGestures( + verse = verseNumber, + onVerseTap = onVerseTap, + onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress ) - ) + .onGloballyPositioned { coordinates -> + onVersePositioned( + verseNumber, + VerseLayoutInfo( + topPx = coordinates.positionInParent().y.toInt(), + heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) + ) + ) + } + ) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "$verseNumber ${pair.first}", + style = TextStyle( + fontSize = bibleState.fontSize.sp, + fontFamily = if (bibleState.isFontFamilySerif) bibleState.mainTranslation.language.serifFontFamily() else bibleState.mainTranslation.language.sansFontFamily(), + color = mainTextColor + ), + modifier = Modifier + .weight(1f) + .background(mainBackground) + ) + Text( + text = "$verseNumber ${pair.second}", + style = TextStyle( + fontSize = bibleState.fontSize.sp, + fontFamily = if (bibleState.isFontFamilySerif) bibleState.subTranslation.language.serifFontFamily() else bibleState.subTranslation.language.sansFontFamily(), + color = subTextColor + ), + modifier = Modifier + .weight(1f) + .background(subBackground) + ) + } + Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) } - ) { - Row( - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "$verseNumber ${pair.first}", - style = TextStyle( - fontSize = bibleState.fontSize.sp, - fontFamily = if (bibleState.isFontFamilySerif) bibleState.mainTranslation.language.serifFontFamily() else bibleState.mainTranslation.language.sansFontFamily(), - color = mainTextColor - ), - modifier = Modifier - .weight(1f) - .background(mainBackground) - ) - Text( - text = "$verseNumber ${pair.second}", - style = TextStyle( - fontSize = bibleState.fontSize.sp, - fontFamily = if (bibleState.isFontFamilySerif) bibleState.subTranslation.language.serifFontFamily() else bibleState.subTranslation.language.sansFontFamily(), - color = subTextColor - ), - modifier = Modifier - .weight(1f) - .background(subBackground) - ) } - Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) } } } From 33875f249d502153dbe1c9fee47cdf98a8d253ea Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:56:17 +0900 Subject: [PATCH 23/37] app: enable platform text selection in under reader --- .../app/ui/widgets/BilingualUnderBible.kt | 139 +++++++++--------- 1 file changed, 72 insertions(+), 67 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt index f1eefd8d..bc71f848 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -39,7 +40,7 @@ fun BilingualUnderBible( selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, - onVerseLongPress: (Int) -> Unit = {}, + onVerseLongPress: ((Int) -> Unit)? = null, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -57,76 +58,80 @@ fun BilingualUnderBible( bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap ) { - versePairs.forEachIndexed { verse, pair -> - val animatedMainBackground = animatedBilingualUnderTranslationBackgroundColor( - bibleState = bibleState, - verseIndex = verse, - highlightedVerse = highlightedVerse, - isSubTranslation = false - ).value - val animatedSubBackground = animatedBilingualUnderTranslationBackgroundColor( - bibleState = bibleState, - verseIndex = verse, - highlightedVerse = highlightedVerse, - isSubTranslation = true - ).value - val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value - val verseNumber = verse + 1 - val isMainSelected = selectedTextSelection?.containsBilingualVersePart( - verse = verseNumber, - isSubTranslation = false - ) == true - val isSubSelected = selectedTextSelection?.containsBilingualVersePart( - verse = verseNumber, - isSubTranslation = true - ) == true - val mainBackground = if (isMainSelected) MaterialTheme.colorScheme.primaryContainer else animatedMainBackground - val subBackground = if (isSubSelected) MaterialTheme.colorScheme.primaryContainer else animatedSubBackground - val mainTextColor = if (isMainSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor - val subTextColor = if (isSubSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor - - Column( - modifier = Modifier - .fillMaxWidth() - .verseTapGestures( + SelectionContainer { + Column { + versePairs.forEachIndexed { verse, pair -> + val animatedMainBackground = animatedBilingualUnderTranslationBackgroundColor( + bibleState = bibleState, + verseIndex = verse, + highlightedVerse = highlightedVerse, + isSubTranslation = false + ).value + val animatedSubBackground = animatedBilingualUnderTranslationBackgroundColor( + bibleState = bibleState, + verseIndex = verse, + highlightedVerse = highlightedVerse, + isSubTranslation = true + ).value + val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value + val verseNumber = verse + 1 + val isMainSelected = selectedTextSelection?.containsBilingualVersePart( + verse = verseNumber, + isSubTranslation = false + ) == true + val isSubSelected = selectedTextSelection?.containsBilingualVersePart( verse = verseNumber, - onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress - ) - .onGloballyPositioned { coordinates -> - onVersePositioned( - verseNumber, - VerseLayoutInfo( - topPx = coordinates.positionInParent().y.toInt(), - heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) + isSubTranslation = true + ) == true + val mainBackground = if (isMainSelected) MaterialTheme.colorScheme.primaryContainer else animatedMainBackground + val subBackground = if (isSubSelected) MaterialTheme.colorScheme.primaryContainer else animatedSubBackground + val mainTextColor = if (isMainSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor + val subTextColor = if (isSubSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor + + Column( + modifier = Modifier + .fillMaxWidth() + .verseTapGestures( + verse = verseNumber, + onVerseTap = onVerseTap, + onVerseDoubleTap = onVerseDoubleTap, + onVerseLongPress = onVerseLongPress ) + .onGloballyPositioned { coordinates -> + onVersePositioned( + verseNumber, + VerseLayoutInfo( + topPx = coordinates.positionInParent().y.toInt(), + heightPx = (coordinates.size.height - verseSpacingPx).coerceAtLeast(0) + ) + ) + } + ) { + Text( + text = "$verseNumber ${pair.first}", + style = TextStyle( + fontSize = bibleState.fontSize.sp, + fontFamily = if (bibleState.isFontFamilySerif) bibleState.mainTranslation.language.serifFontFamily() else bibleState.mainTranslation.language.sansFontFamily(), + color = mainTextColor + ), + modifier = Modifier + .fillMaxWidth() + .background(mainBackground) + ) + Text( + text = "$verseNumber ${pair.second}", + style = TextStyle( + fontSize = bibleState.fontSize.sp, + fontFamily = if (bibleState.isFontFamilySerif) bibleState.subTranslation.language.serifFontFamily() else bibleState.subTranslation.language.sansFontFamily(), + color = subTextColor + ), + modifier = Modifier + .fillMaxWidth() + .background(subBackground) ) + Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) } - ) { - Text( - text = "$verseNumber ${pair.first}", - style = TextStyle( - fontSize = bibleState.fontSize.sp, - fontFamily = if (bibleState.isFontFamilySerif) bibleState.mainTranslation.language.serifFontFamily() else bibleState.mainTranslation.language.sansFontFamily(), - color = mainTextColor - ), - modifier = Modifier - .fillMaxWidth() - .background(mainBackground) - ) - Text( - text = "$verseNumber ${pair.second}", - style = TextStyle( - fontSize = bibleState.fontSize.sp, - fontFamily = if (bibleState.isFontFamilySerif) bibleState.subTranslation.language.serifFontFamily() else bibleState.subTranslation.language.sansFontFamily(), - color = subTextColor - ), - modifier = Modifier - .fillMaxWidth() - .background(subBackground) - ) - Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) + } } } } From 91d2d09c79cc81404d469a9619327964d520e977 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:57:17 +0900 Subject: [PATCH 24/37] app: use platform text selection handles --- .../kotlin/org/gnit/bible/app/screens/ReadingScreen.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt index 993278af..cab84e68 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt @@ -308,7 +308,6 @@ fun BibleReadingArea( selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress, topContentPadding = topContentPadding, bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap @@ -325,7 +324,6 @@ fun BibleReadingArea( selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress, topContentPadding = topContentPadding, bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap @@ -341,7 +339,6 @@ fun BibleReadingArea( selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress, topContentPadding = topContentPadding, bottomContentPadding = bottomContentPadding, onTitleTap = onTitleTap From 652b1e03150701813bd11b960650ecd2895bc10a Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 17:57:45 +0900 Subject: [PATCH 25/37] app: fix nullable long press callback type --- .../kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt index ed61b3b9..7e700260 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt @@ -53,7 +53,7 @@ internal fun Modifier.verseTapGestures( onTap = { onVerseTap(verse) }, onDoubleTap = { onVerseDoubleTap(verse) }, onLongPress = onVerseLongPress?.let { longPress -> - { longPress(verse) } + { _ -> longPress(verse) } } ) } From 2c77dead7050615326e55c7f9de2cc01d019d27e Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:12:59 +0900 Subject: [PATCH 26/37] app: remove custom selection popup code --- .../gnit/bible/app/screens/ReadingScreen.kt | 158 +----------------- 1 file changed, 1 insertion(+), 157 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt index cab84e68..251f1fb0 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/screens/ReadingScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -16,14 +15,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -43,14 +38,10 @@ import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.isCtrlPressed import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import kotlinx.coroutines.Dispatchers @@ -70,7 +61,6 @@ import org.gnit.bible.app.ui.widgets.BilingualUnderBible import org.gnit.bible.app.ui.widgets.SingleBible import org.gnit.bible.app.ui.widgets.sansFontFamily import org.gnit.bible.app.ui.widgets.serifFontFamily -import org.jetbrains.compose.resources.painterResource import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt @@ -83,10 +73,6 @@ const val READING_SCREEN_VERTICAL_SPACE = 1 const val BOOK_CONTROLS_BAR_BOTTOM_MARGIN = READING_SCREEN_VERTICAL_SPACE const val CHAPTER_CONTROLS_BAR_TOP_MARGIN = READING_SCREEN_VERTICAL_SPACE const val READING_SCREEN_HORIZONTAL_PADDING = 4 -const val SELECTION_POPUP_ICON_SIZE: Int = 20 -const val SELECTION_POPUP_PADDING: Int = 6 -const val SELECTION_POPUP_GAP: Int = 0 -private const val SELECTION_POPUP_HEIGHT: Int = SELECTION_POPUP_ICON_SIZE + (SELECTION_POPUP_PADDING * 2) @Composable fun BibleReadingArea( @@ -119,16 +105,6 @@ fun BibleReadingArea( mutableStateMapOf() } var viewportHeight by remember { mutableStateOf(0) } - var selectedTextSelection by remember( - state.book, - state.chapter, - state.readingMode, - state.mainTranslation, - state.subTranslation - ) { - mutableStateOf(null) - } - val clipboardManager = LocalClipboardManager.current LaunchedEffect(scrollState) { snapshotFlow { scrollState.isScrollInProgress }.collect { inProgress -> @@ -214,15 +190,12 @@ fun BibleReadingArea( onStateChange(currentState.copy(scrollPercent = scrollPercent)) } val onTitleTap: () -> Unit = { - selectedTextSelection = null if (!currentState.isSearchActive) { onSearchRequested() } } val onVerseTap: (Int) -> Unit = { - if (selectedTextSelection != null) { - selectedTextSelection = null - } else if (currentState.isSearchActive) { + if (currentState.isSearchActive) { onSearchCancel() } else if (chrome.isVisible()) { chrome.forceHide() @@ -231,50 +204,9 @@ fun BibleReadingArea( } } val onVerseDoubleTap: (Int) -> Unit = { verse -> - selectedTextSelection = null onStateChange(currentState.recordReadHistory(verse)) chrome.onUserInteraction() } - val onVerseLongPress: (Int) -> Unit = { verse -> - selectedTextSelection = when (val content = readingContent) { - is ReadingContent.Single -> BibleTextSelection.singleVerse( - verse = verse, - verseCount = content.verses.size - ) - - is ReadingContent.Bilingual -> BibleTextSelection.bilingualMainVerse( - verse = verse, - verseCount = content.versePairs.size - ) - - null -> null - } - if (selectedTextSelection != null) { - chrome.forceHide() - } - } - val copySelectedText: () -> Unit = { - val selection = selectedTextSelection - if (selection != null) { - val selectedText = when (val content = readingContent) { - is ReadingContent.Single -> selection.copySingleText( - bibleState = currentState, - verses = content.verses - ) - - is ReadingContent.Bilingual -> selection.copyBilingualText( - bibleState = currentState, - versePairs = content.versePairs - ) - - null -> "" - } - if (selectedText.isNotBlank()) { - clipboardManager.setText(AnnotatedString(selectedText)) - } - } - selectedTextSelection = null - } Box( modifier = modifier @@ -305,7 +237,6 @@ fun BibleReadingArea( onScrollPercentChange = onScrollPercentChange, onVersePositioned = { verse, layout -> verseLayouts[verse] = layout }, highlightedVerse = state.highlightedVerse, - selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, topContentPadding = topContentPadding, @@ -321,7 +252,6 @@ fun BibleReadingArea( onScrollPercentChange = onScrollPercentChange, onVersePositioned = { verse, layout -> verseLayouts[verse] = layout }, highlightedVerse = state.highlightedVerse, - selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, topContentPadding = topContentPadding, @@ -336,7 +266,6 @@ fun BibleReadingArea( onScrollPercentChange = onScrollPercentChange, onVersePositioned = { verse, layout -> verseLayouts[verse] = layout }, highlightedVerse = state.highlightedVerse, - selectedTextSelection = selectedTextSelection, onVerseTap = onVerseTap, onVerseDoubleTap = onVerseDoubleTap, topContentPadding = topContentPadding, @@ -347,16 +276,6 @@ fun BibleReadingArea( ReadingMode.SINGLE -> Unit } } - - selectedTextSelection?.let { selection -> - BibleTextSelectionPopup( - selectedVerseLayout = verseLayouts[selection.anchorVerse], - scrollState = scrollState, - viewportHeight = viewportHeight, - onCopy = copySelectedText, - onSelectAll = { selectedTextSelection = selection.selectAll() } - ) - } } LaunchedEffect(state.highlightedVerse) { @@ -381,81 +300,6 @@ fun BibleReadingArea( } } -@Composable -private fun BibleTextSelectionPopup( - selectedVerseLayout: VerseLayoutInfo?, - scrollState: ScrollState, - viewportHeight: Int, - onCopy: () -> Unit, - onSelectAll: () -> Unit -) { - if (selectedVerseLayout == null || viewportHeight <= 0) return - - val density = LocalDensity.current - val popupHeightPx = with(density) { SELECTION_POPUP_HEIGHT.dp.roundToPx() } - val gapPx = with(density) { SELECTION_POPUP_GAP.dp.roundToPx() } - val verseTopInViewport = selectedVerseLayout.topPx - scrollState.value - val shouldShowAbove = verseTopInViewport > viewportHeight / 2 - val rawPopupY = if (shouldShowAbove) { - verseTopInViewport - popupHeightPx - gapPx - } else { - verseTopInViewport + selectedVerseLayout.heightPx + gapPx - } - val maxPopupY = (viewportHeight - popupHeightPx).coerceAtLeast(0) - val popupY = rawPopupY.coerceIn(0, maxPopupY) - - Box( - modifier = Modifier - .fillMaxWidth() - .offset { IntOffset(0, popupY) }, - contentAlignment = Alignment.Center - ) { - Surface( - shape = RoundedCornerShape(10.dp), - tonalElevation = 6.dp, - shadowElevation = 6.dp, - color = MaterialTheme.colorScheme.surfaceVariant - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp)) - SelectionPopupIconButton(onClick = onCopy) { - Icon( - painter = painterResource(Res.drawable.content_copy), - contentDescription = null, - modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp) - ) - } - Spacer(modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp)) - SelectionPopupIconButton(onClick = onSelectAll) { - Icon( - painter = painterResource(Res.drawable.select_all), - contentDescription = null, - modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp) - ) - } - Spacer(modifier = Modifier.size(SELECTION_POPUP_ICON_SIZE.dp)) - } - } - } -} - -@Composable -private fun SelectionPopupIconButton( - onClick: () -> Unit, - content: @Composable () -> Unit -) { - Box( - modifier = Modifier - .clickable(onClick = onClick) - .padding(SELECTION_POPUP_PADDING.dp), - contentAlignment = Alignment.Center - ) { - content() - } -} - private sealed interface ReadingContent { data class Single(val verses: Array) : ReadingContent data class Bilingual(val versePairs: List>) : ReadingContent From fc01d86b039d4988c66eb03b236eaa63b46a4eba Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:13:13 +0900 Subject: [PATCH 27/37] app: remove custom selection state from single reader --- .../org/gnit/bible/app/ui/widgets/SingleBible.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt index 1b5d99b3..d7f96c78 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -19,7 +18,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import org.gnit.bible.app.BibleTextSelection import org.gnit.bible.app.ScrollableColumn import org.gnit.bible.app.VerseLayoutInfo import org.gnit.bible.app.state.BibleState @@ -34,7 +32,6 @@ fun SingleBible( onScrollPercentChange: (Float) -> Unit = {}, onVersePositioned: (Int, VerseLayoutInfo) -> Unit = { _, _ -> }, highlightedVerse: Int? = null, - selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, onVerseLongPress: ((Int) -> Unit)? = null, @@ -58,9 +55,6 @@ fun SingleBible( verses.forEachIndexed { verse, text -> val animatedBackground = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value - val isSelected = selectedTextSelection?.containsSingleVerse(verse + 1) == true - val background = if (isSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground - val textColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor Column( modifier = Modifier @@ -90,11 +84,11 @@ fun SingleBible( } else { translation.language.sansFontFamily() }, - color = textColor + color = animatedTextColor ), modifier = Modifier .fillMaxWidth() - .background(background) + .background(animatedBackground) ) Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) } From 5fe75e953d09b08cd150887779731e5718556491 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:13:29 +0900 Subject: [PATCH 28/37] app: remove custom selection state from side reader --- .../app/ui/widgets/BilingualSideBible.kt | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt index b93636cf..d601664b 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -22,7 +21,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import org.gnit.bible.app.BibleTextSelection import org.gnit.bible.app.ScrollableColumn import org.gnit.bible.app.VerseLayoutInfo import org.gnit.bible.app.state.BibleState @@ -38,7 +36,6 @@ fun BilingualSideBible( onScrollPercentChange: (Float) -> Unit = {}, onVersePositioned: (Int, VerseLayoutInfo) -> Unit = { _, _ -> }, highlightedVerse: Int? = null, - selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, onVerseLongPress: ((Int) -> Unit)? = null, @@ -65,18 +62,6 @@ fun BilingualSideBible( val animatedBackground = animatedVerseBackgroundColor(bibleState, verse, highlightedVerse).value val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value val verseNumber = verse + 1 - val isMainSelected = selectedTextSelection?.containsBilingualVersePart( - verse = verseNumber, - isSubTranslation = false - ) == true - val isSubSelected = selectedTextSelection?.containsBilingualVersePart( - verse = verseNumber, - isSubTranslation = true - ) == true - val mainBackground = if (isMainSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground - val subBackground = if (isSubSelected) MaterialTheme.colorScheme.primaryContainer else animatedBackground - val mainTextColor = if (isMainSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor - val subTextColor = if (isSubSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor Column( modifier = Modifier @@ -105,22 +90,22 @@ fun BilingualSideBible( style = TextStyle( fontSize = bibleState.fontSize.sp, fontFamily = if (bibleState.isFontFamilySerif) bibleState.mainTranslation.language.serifFontFamily() else bibleState.mainTranslation.language.sansFontFamily(), - color = mainTextColor + color = animatedTextColor ), modifier = Modifier .weight(1f) - .background(mainBackground) + .background(animatedBackground) ) Text( text = "$verseNumber ${pair.second}", style = TextStyle( fontSize = bibleState.fontSize.sp, fontFamily = if (bibleState.isFontFamilySerif) bibleState.subTranslation.language.serifFontFamily() else bibleState.subTranslation.language.sansFontFamily(), - color = subTextColor + color = animatedTextColor ), modifier = Modifier .weight(1f) - .background(subBackground) + .background(animatedBackground) ) } Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) From d70d87eaa8ad6203eb5255a2e2ea259e541d572a Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:13:43 +0900 Subject: [PATCH 29/37] app: remove custom selection state from under reader --- .../app/ui/widgets/BilingualUnderBible.kt | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt index bc71f848..cc8d1fd2 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -21,7 +20,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import org.gnit.bible.app.BibleTextSelection import org.gnit.bible.app.ScrollableColumn import org.gnit.bible.app.VerseLayoutInfo import org.gnit.bible.app.state.BibleState @@ -37,7 +35,6 @@ fun BilingualUnderBible( onScrollPercentChange: (Float) -> Unit = {}, onVersePositioned: (Int, VerseLayoutInfo) -> Unit = { _, _ -> }, highlightedVerse: Int? = null, - selectedTextSelection: BibleTextSelection? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, onVerseLongPress: ((Int) -> Unit)? = null, @@ -75,18 +72,6 @@ fun BilingualUnderBible( ).value val animatedTextColor = animatedVerseTextColor(verse, highlightedVerse).value val verseNumber = verse + 1 - val isMainSelected = selectedTextSelection?.containsBilingualVersePart( - verse = verseNumber, - isSubTranslation = false - ) == true - val isSubSelected = selectedTextSelection?.containsBilingualVersePart( - verse = verseNumber, - isSubTranslation = true - ) == true - val mainBackground = if (isMainSelected) MaterialTheme.colorScheme.primaryContainer else animatedMainBackground - val subBackground = if (isSubSelected) MaterialTheme.colorScheme.primaryContainer else animatedSubBackground - val mainTextColor = if (isMainSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor - val subTextColor = if (isSubSelected) MaterialTheme.colorScheme.onPrimaryContainer else animatedTextColor Column( modifier = Modifier @@ -112,22 +97,22 @@ fun BilingualUnderBible( style = TextStyle( fontSize = bibleState.fontSize.sp, fontFamily = if (bibleState.isFontFamilySerif) bibleState.mainTranslation.language.serifFontFamily() else bibleState.mainTranslation.language.sansFontFamily(), - color = mainTextColor + color = animatedTextColor ), modifier = Modifier .fillMaxWidth() - .background(mainBackground) + .background(animatedMainBackground) ) Text( text = "$verseNumber ${pair.second}", style = TextStyle( fontSize = bibleState.fontSize.sp, fontFamily = if (bibleState.isFontFamilySerif) bibleState.subTranslation.language.serifFontFamily() else bibleState.subTranslation.language.sansFontFamily(), - color = subTextColor + color = animatedTextColor ), modifier = Modifier .fillMaxWidth() - .background(subBackground) + .background(animatedSubBackground) ) Spacer(modifier = Modifier.height(bibleState.spaceBetweenVerses.dp)) } From b6149e80f97f3b22749226bc75cd669161a6a730 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:14:02 +0900 Subject: [PATCH 30/37] app: remove unused selection drawable --- .../commonMain/composeResources/drawable/select_all.xml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 app/shared/src/commonMain/composeResources/drawable/select_all.xml diff --git a/app/shared/src/commonMain/composeResources/drawable/select_all.xml b/app/shared/src/commonMain/composeResources/drawable/select_all.xml deleted file mode 100644 index 22258b0c..00000000 --- a/app/shared/src/commonMain/composeResources/drawable/select_all.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - From a3291affa8afc25f84874f3aca3572273c8a836d Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:14:27 +0900 Subject: [PATCH 31/37] remove unused drawable --- .../composeResources/drawable/content_copy.xml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 app/shared/src/commonMain/composeResources/drawable/content_copy.xml diff --git a/app/shared/src/commonMain/composeResources/drawable/content_copy.xml b/app/shared/src/commonMain/composeResources/drawable/content_copy.xml deleted file mode 100644 index 492c0e6c..00000000 --- a/app/shared/src/commonMain/composeResources/drawable/content_copy.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - From 68fbe730db5ed482a630596ac5682320adf047ab Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:14:34 +0900 Subject: [PATCH 32/37] remove custom selection model --- .../org/gnit/bible/app/BibleTextSelection.kt | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt deleted file mode 100644 index 495c7d1e..00000000 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/BibleTextSelection.kt +++ /dev/null @@ -1,105 +0,0 @@ -package org.gnit.bible.app - -import org.gnit.bible.Translation -import org.gnit.bible.app.state.BibleState - -data class BibleTextSelection( - val firstUnit: Int, - val lastUnit: Int, - val unitCount: Int, - val unitsPerVerse: Int, - val anchorUnit: Int = firstUnit -) { - val anchorVerse: Int - get() { - if (unitCount <= 0) return 1 - return (anchorUnit.coerceIn(0, unitCount - 1) / unitsPerVerse) + 1 - } - - fun containsSingleVerse(verse: Int): Boolean { - if (unitsPerVerse != SINGLE_UNITS_PER_VERSE) return false - return (verse - 1) in selectedUnitRange() - } - - fun containsBilingualVersePart(verse: Int, isSubTranslation: Boolean): Boolean { - if (unitsPerVerse != BILINGUAL_UNITS_PER_VERSE) return false - val unit = ((verse - 1) * BILINGUAL_UNITS_PER_VERSE) + if (isSubTranslation) 1 else 0 - return unit in selectedUnitRange() - } - - fun selectAll(): BibleTextSelection { - if (unitCount <= 0) return this - return copy(firstUnit = 0, lastUnit = unitCount - 1) - } - - fun copySingleText(bibleState: BibleState, verses: Array): String { - val selectedVerses = selectedUnitRange() - .map { unit -> unit + 1 } - .filter { verse -> verse in 1..verses.size } - if (selectedVerses.isEmpty()) return "" - - return buildString { - appendLine(bibleState.describeBookChapter()) - selectedVerses.forEach { verse -> - appendLine("$verse ${verses[verse - 1]}") - } - }.trimEnd() - } - - fun copyBilingualText(bibleState: BibleState, versePairs: List>): String { - val subTranslation = bibleState.subTranslation ?: return "" - val selectedUnits = selectedUnitRange() - .filter { unit -> unit / BILINGUAL_UNITS_PER_VERSE in versePairs.indices } - if (selectedUnits.isEmpty()) return "" - - return buildString { - appendLine(bibleState.describeBookChapter()) - selectedUnits.forEach { unit -> - val verseIndex = unit / BILINGUAL_UNITS_PER_VERSE - val isSubTranslation = unit % BILINGUAL_UNITS_PER_VERSE == 1 - val verseNumber = verseIndex + 1 - val translation = if (isSubTranslation) subTranslation else bibleState.mainTranslation - val verseText = if (isSubTranslation) versePairs[verseIndex].second else versePairs[verseIndex].first - appendLine("$verseNumber ${translation.copyLabel()} $verseText") - } - }.trimEnd() - } - - private fun selectedUnitRange(): IntRange { - if (unitCount <= 0) return IntRange.EMPTY - val first = minOf(firstUnit, lastUnit).coerceIn(0, unitCount - 1) - val last = maxOf(firstUnit, lastUnit).coerceIn(0, unitCount - 1) - return first..last - } - - private fun Translation.copyLabel(): String = "[${code.uppercase()}]" - - companion object { - private const val SINGLE_UNITS_PER_VERSE = 1 - private const val BILINGUAL_UNITS_PER_VERSE = 2 - - fun singleVerse(verse: Int, verseCount: Int): BibleTextSelection { - val unitCount = verseCount.coerceAtLeast(0) - val unit = (verse - 1).coerceIn(0, (unitCount - 1).coerceAtLeast(0)) - return BibleTextSelection( - firstUnit = unit, - lastUnit = unit, - unitCount = unitCount, - unitsPerVerse = SINGLE_UNITS_PER_VERSE, - anchorUnit = unit - ) - } - - fun bilingualMainVerse(verse: Int, verseCount: Int): BibleTextSelection { - val unitCount = (verseCount * BILINGUAL_UNITS_PER_VERSE).coerceAtLeast(0) - val unit = ((verse - 1) * BILINGUAL_UNITS_PER_VERSE).coerceIn(0, (unitCount - 1).coerceAtLeast(0)) - return BibleTextSelection( - firstUnit = unit, - lastUnit = unit, - unitCount = unitCount, - unitsPerVerse = BILINGUAL_UNITS_PER_VERSE, - anchorUnit = unit - ) - } - } -} From 513f2a72b77894d47e167147cc89d03cd0bc0a6e Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:18:09 +0900 Subject: [PATCH 33/37] app: remove custom long press hook --- .../gnit/bible/app/ui/widgets/BibleReadingHelpers.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt index 7e700260..189c7e04 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt @@ -45,16 +45,12 @@ private fun normalBilingualUnderTranslationBackgroundColor( internal fun Modifier.verseTapGestures( verse: Int, onVerseTap: (Int) -> Unit, - onVerseDoubleTap: (Int) -> Unit, - onVerseLongPress: ((Int) -> Unit)? = null + onVerseDoubleTap: (Int) -> Unit ): Modifier { - return pointerInput(verse, onVerseTap, onVerseDoubleTap, onVerseLongPress) { + return pointerInput(verse, onVerseTap, onVerseDoubleTap) { detectTapGestures( onTap = { onVerseTap(verse) }, - onDoubleTap = { onVerseDoubleTap(verse) }, - onLongPress = onVerseLongPress?.let { longPress -> - { _ -> longPress(verse) } - } + onDoubleTap = { onVerseDoubleTap(verse) } ) } } @@ -123,7 +119,7 @@ internal fun animatedVerseTextColor( fun addEmptyEntryToMakeSameSize( listA: List, listB: List -): Pair, List> { +): Pair, String> { val longerList = if (listA.size > listB.size) listA else listB val shorterList = if (listA.size < listB.size) listA else listB From 4718b6ed5563c38dab31360243d69f119a80f4da Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:18:31 +0900 Subject: [PATCH 34/37] app: fix verse padding helper type --- .../kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt index 189c7e04..47ccbad3 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BibleReadingHelpers.kt @@ -119,7 +119,7 @@ internal fun animatedVerseTextColor( fun addEmptyEntryToMakeSameSize( listA: List, listB: List -): Pair, String> { +): Pair, List> { val longerList = if (listA.size > listB.size) listA else listB val shorterList = if (listA.size < listB.size) listA else listB From f065f431461cc15573f9b6e57a2fb1694e0be175 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:18:44 +0900 Subject: [PATCH 35/37] app: remove single reader long press parameter --- .../kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt index d7f96c78..a873dcce 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/SingleBible.kt @@ -34,7 +34,6 @@ fun SingleBible( highlightedVerse: Int? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, - onVerseLongPress: ((Int) -> Unit)? = null, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -62,8 +61,7 @@ fun SingleBible( .verseTapGestures( verse = verse + 1, onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress + onVerseDoubleTap = onVerseDoubleTap ) .onGloballyPositioned { coordinates -> onVersePositioned( From 4899c90f478d0cf70e6b1f306a6e966f4f1faeb8 Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:18:58 +0900 Subject: [PATCH 36/37] app: remove side reader long press parameter --- .../org/gnit/bible/app/ui/widgets/BilingualSideBible.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt index d601664b..b4e8d58a 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualSideBible.kt @@ -38,7 +38,6 @@ fun BilingualSideBible( highlightedVerse: Int? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, - onVerseLongPress: ((Int) -> Unit)? = null, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -69,8 +68,7 @@ fun BilingualSideBible( .verseTapGestures( verse = verseNumber, onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress + onVerseDoubleTap = onVerseDoubleTap ) .onGloballyPositioned { coordinates -> onVersePositioned( From 9e667163e6def045345b08ee9e31cbd9f72ec8fa Mon Sep 17 00:00:00 2001 From: Hokuto Joel Ide Date: Wed, 8 Jul 2026 18:19:12 +0900 Subject: [PATCH 37/37] app: remove under reader long press parameter --- .../org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt index cc8d1fd2..14de2516 100644 --- a/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt +++ b/app/shared/src/commonMain/kotlin/org/gnit/bible/app/ui/widgets/BilingualUnderBible.kt @@ -37,7 +37,6 @@ fun BilingualUnderBible( highlightedVerse: Int? = null, onVerseTap: (Int) -> Unit = {}, onVerseDoubleTap: (Int) -> Unit = {}, - onVerseLongPress: ((Int) -> Unit)? = null, topContentPadding: Dp = 0.dp, bottomContentPadding: Dp = 0.dp, onTitleTap: () -> Unit = {} @@ -79,8 +78,7 @@ fun BilingualUnderBible( .verseTapGestures( verse = verseNumber, onVerseTap = onVerseTap, - onVerseDoubleTap = onVerseDoubleTap, - onVerseLongPress = onVerseLongPress + onVerseDoubleTap = onVerseDoubleTap ) .onGloballyPositioned { coordinates -> onVersePositioned(