~repos /only-bible-app
git clone
https://pyrossh.dev/repos/only-bible-app.git
The only bible app you will ever need. No ads. No in-app purchases. No distractions.
file:
composeApp/src/commonMain/kotlin/dev/pyrossh/onlyBible/composables/VerseText.kt
package dev.pyrossh.onlyBible.composables
import dev.pyrossh.onlyBible.AppViewModelimport dev.pyrossh.onlyBible.screens.ChapterScreenPropsimport dev.pyrossh.onlyBible.FontTypeimport androidx.compose.foundation.borderimport androidx.compose.foundation.clickableimport androidx.compose.foundation.interaction.MutableInteractionSourceimport androidx.compose.foundation.isSystemInDarkThemeimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.layout.widthimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.automirrored.outlined.OpenInNewimport androidx.compose.material.icons.filled.Circleimport androidx.compose.material.icons.outlined.Cancelimport androidx.compose.material.icons.outlined.PauseCircleimport androidx.compose.material.icons.outlined.PlayCircleimport androidx.compose.material.icons.outlined.Shareimport androidx.compose.material3.Iconimport androidx.compose.material3.IconButtonimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.collectAsStateimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.draw.shadowimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.layout.onPlacedimport androidx.compose.ui.layout.positionInRootimport androidx.compose.ui.text.SpanStyleimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.text.buildAnnotatedStringimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.text.withStyleimport androidx.compose.ui.unit.IntOffsetimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.spimport androidx.compose.ui.window.Popupimport dev.pyrossh.onlyBible.Platformimport dev.pyrossh.onlyBible.ShareKitimport dev.pyrossh.onlyBible.SpeechServiceimport dev.pyrossh.onlyBible.darkHighlightsimport dev.pyrossh.onlyBible.domain.Verseimport dev.pyrossh.onlyBible.getPlatformimport dev.pyrossh.onlyBible.isLightThemeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.IOimport kotlinx.coroutines.launchimport dev.pyrossh.onlyBible.lightHighlightsimport dev.pyrossh.onlyBible.utils.SimpleParserimport dev.pyrossh.onlyBible.utils.TagNodeimport dev.pyrossh.onlyBible.utils.TextNodeimport utils.LocalNavController
@Composablefun VerseText( model: AppViewModel, fontType: FontType, fontSizeDelta: Int, fontBoldEnabled: Boolean, verse: Verse, highlightWord: String?,) { var barYPosition by remember { mutableIntStateOf(0) } val selectedVerses by model.selectedVerses.collectAsState() val isLight = isLightTheme(model.themeType, isSystemInDarkTheme()) val buttonInteractionSource = remember { MutableInteractionSource() } val isSelected = selectedVerses.contains(verse) val highlightedColorIndex = model.getHighlightForVerse(verse) val currentHighlightColors = if (isLight) lightHighlights else darkHighlights val text = if (highlightWord != null) verse.text.replace( highlightWord, "<yellow>${highlightWord}</yellow>", true ) else verse.text val nodes = SimpleParser(text).parse() Text( modifier = Modifier .onPlaced { barYPosition = it.positionInRoot().y.toInt() + it.size.height } .clickable( interactionSource = buttonInteractionSource, indication = null ) { model.setSelectedVerses( if (selectedVerses.contains(verse)) { selectedVerses - verse } else { selectedVerses + verse } ) }, style = TextStyle( background = if (isSelected) MaterialTheme.colorScheme.outlineVariant else if (highlightedColorIndex != null && isLight) currentHighlightColors[highlightedColorIndex] else Color.Unspecified, fontFamily = fontType.family(), color = if (isLight) Color(0xFF000104) else if (highlightedColorIndex != null) currentHighlightColors[highlightedColorIndex] else Color(0xFFBCBCBC), fontWeight = if (fontBoldEnabled) FontWeight.W700 else FontWeight.W400, fontSize = (17 + fontSizeDelta).sp, lineHeight = (23 + fontSizeDelta).sp, letterSpacing = 0.sp, ), text = buildAnnotatedString { withStyle( style = SpanStyle( fontSize = (13 + fontSizeDelta).sp, color = if (isLight) Color(0xFFA20101) else Color(0xFFCCCCCC), fontWeight = FontWeight.W700, ) ) { append("${verse.verseIndex + 1} ") } for (n in nodes) { if (n is TextNode) { append(n.value) } else if (n is TagNode && n.name == "br") { append("\n") } else if (n is TagNode && n.name == "red") { withStyle( style = SpanStyle( color = Color.Red, ) ) { append(n.child!!.value) } } else if (n is TagNode && n.name == "yellow") { withStyle( style = SpanStyle( background = Color.Yellow, ) ) { append(n.child!!.value) } } } } ) if (isSelected && selectedVerses.last() == verse) { Menu( model = model, barYPosition = barYPosition, verse = verse, highlightWord = highlightWord, ) }}
@Composableprivate fun Menu( model: AppViewModel, barYPosition: Int, verse: Verse, highlightWord: String?,) { val navController = LocalNavController.current val scope = rememberCoroutineScope() val selectedVerses by model.selectedVerses.collectAsState() Popup( alignment = Alignment.TopCenter, offset = IntOffset(0, y = barYPosition), ) { Surface( color = MaterialTheme.colorScheme.surface, shadowElevation = 0.5.dp, tonalElevation = 0.5.dp, modifier = Modifier .width(if (highlightWord != null) 360.dp else 300.dp) .height(56.dp) .border( width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = RoundedCornerShape(4.dp) ) .shadow( elevation = 2.dp, spotColor = MaterialTheme.colorScheme.outline, ambientColor = MaterialTheme.colorScheme.outline, shape = RoundedCornerShape(4.dp) ), ) { Row( modifier = Modifier .fillMaxSize() .padding(horizontal = 4.dp), horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically, ) { IconButton(onClick = {// view.playSoundEffect(SoundEffectConstants.CLICK) model.removeHighlightedVerses(selectedVerses) model.setSelectedVerses(listOf()) }) { Icon( imageVector = Icons.Outlined.Cancel, contentDescription = "Clear", ) } lightHighlights.forEachIndexed { i, tint -> IconButton(onClick = {// view.playSoundEffect(SoundEffectConstants.CLICK) model.addHighlightedVerses(selectedVerses, i) model.setSelectedVerses(listOf()) }) { Icon( modifier = Modifier .size(20.dp) .border( width = 1.dp, color = MaterialTheme.colorScheme.outline, shape = RoundedCornerShape(24.dp) ), imageVector = Icons.Filled.Circle, contentDescription = "highlight", tint = tint, ) } } if (getPlatform() == Platform.Android) { IconButton(onClick = {// view.playSoundEffect(SoundEffectConstants.CLICK) if (model.isAudioPlaying) { SpeechService.stopTextToSpeech() } else { scope.launch(Dispatchers.IO) { for (v in selectedVerses.sortedBy { it.verseIndex }) { SpeechService.startTextToSpeech(model.bible.voiceName, v.text) } } } }) { Icon(// modifier = Modifier.size(36.dp), imageVector = if (model.isAudioPlaying) Icons.Outlined.PauseCircle else Icons.Outlined.PlayCircle, contentDescription = "Audio", ) } } IconButton(onClick = {// view.playSoundEffect(SoundEffectConstants.CLICK) val verses = selectedVerses.sortedBy { it.verseIndex } val versesThrough = if (verses.size >= 3) "${verses.first().verseIndex + 1}-${verses.last().verseIndex + 1}" else verses.map { it.verseIndex + 1 } .joinToString(",") val title = "${verses[0].bookName} ${verses[0].chapterIndex + 1}:${versesThrough}" val text = verses.joinToString("\n") { it.text.replace("<red>", "") .replace("</red>", "") } ShareKit.shareText("$title\n$text") model.setSelectedVerses(listOf()) }) { Icon(// modifier = Modifier.size(32.dp), imageVector = Icons.Outlined.Share, contentDescription = "Share", ) } if (highlightWord != null) { IconButton(onClick = {// view.playSoundEffect(SoundEffectConstants.CLICK) navController.navigate( ChapterScreenProps( bookIndex = verse.bookIndex, chapterIndex = verse.chapterIndex, verseIndex = verse.verseIndex, ) ) }) { Icon(// modifier = Modifier.size(32.dp), imageVector = Icons.AutoMirrored.Outlined.OpenInNew, contentDescription = "Goto", ) } } } } }}