~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.
4a2e3b1b
—
pyrossh 1 year ago
implement Highlighting
- composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/composables/VerseHeading.kt +70 -25
- composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/composables/VerseText.kt +29 -18
- composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/utils/SimpleParser.kt +58 -5
- composeApp/src/commonTest/kotlin/dev/pyrossh/only_bible_app/utils/SimpleParserTest.kt +32 -1
- composeApp/src/iosMain/kotlin/dev/pyrossh/only_bible_app/Platform.ios.kt +14 -1
composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/composables/VerseHeading.kt
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
package dev.pyrossh.only_bible_app.composables
|
|
2
2
|
|
|
3
3
|
import androidx.compose.foundation.layout.padding
|
|
4
|
+
import androidx.compose.foundation.text.ClickableText
|
|
4
5
|
import androidx.compose.material3.MaterialTheme
|
|
5
|
-
import androidx.compose.material3.Text
|
|
6
6
|
import androidx.compose.runtime.Composable
|
|
7
7
|
import androidx.compose.ui.Modifier
|
|
8
|
+
import androidx.compose.ui.graphics.Color
|
|
9
|
+
import androidx.compose.ui.text.SpanStyle
|
|
8
10
|
import androidx.compose.ui.text.TextStyle
|
|
11
|
+
import androidx.compose.ui.text.buildAnnotatedString
|
|
12
|
+
import androidx.compose.ui.text.font.FontStyle
|
|
9
13
|
import androidx.compose.ui.text.font.FontWeight
|
|
14
|
+
import androidx.compose.ui.text.withStyle
|
|
10
15
|
import androidx.compose.ui.unit.dp
|
|
11
16
|
import androidx.compose.ui.unit.sp
|
|
12
17
|
import dev.pyrossh.only_bible_app.FontType
|
|
18
|
+
import dev.pyrossh.only_bible_app.screens.ChapterScreenProps
|
|
19
|
+
import dev.pyrossh.only_bible_app.utils.SimpleParser
|
|
20
|
+
import dev.pyrossh.only_bible_app.utils.TagNode
|
|
21
|
+
import dev.pyrossh.only_bible_app.utils.TextNode
|
|
13
22
|
import utils.LocalNavController
|
|
14
23
|
|
|
15
24
|
@Composable
|
|
@@ -19,7 +28,49 @@ fun VerseHeading(
|
|
|
19
28
|
fontSizeDelta: Int,
|
|
20
29
|
) {
|
|
21
30
|
val navController = LocalNavController.current
|
|
31
|
+
val nodes = SimpleParser(text).parse()
|
|
32
|
+
val annotatedString = buildAnnotatedString {
|
|
33
|
+
for (n in nodes) {
|
|
34
|
+
if (n is TextNode) {
|
|
35
|
+
append(n.value)
|
|
36
|
+
} else if (n is TagNode && n.name == "br") {
|
|
37
|
+
append("\n")
|
|
38
|
+
} else if (n is TagNode && n.name == "red") {
|
|
39
|
+
withStyle(
|
|
40
|
+
style = SpanStyle(
|
|
41
|
+
color = Color.Red,
|
|
42
|
+
)
|
|
43
|
+
) {
|
|
44
|
+
append(n.child!!.value)
|
|
45
|
+
}
|
|
46
|
+
} else if (n is TagNode && n.name == "yellow") {
|
|
47
|
+
withStyle(
|
|
48
|
+
style = SpanStyle(
|
|
49
|
+
background = Color.Yellow,
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
append(n.child!!.value)
|
|
53
|
+
}
|
|
54
|
+
} else if (n is TagNode && n.name == "a") {
|
|
55
|
+
withStyle(
|
|
56
|
+
style = SpanStyle(
|
|
57
|
+
fontSize = (14 + fontSizeDelta).sp,
|
|
58
|
+
fontStyle = FontStyle.Italic,
|
|
59
|
+
color = Color(0xFF008AE6),
|
|
60
|
+
)
|
|
61
|
+
) {
|
|
62
|
+
append(n.child!!.value)
|
|
63
|
+
addStringAnnotation(
|
|
64
|
+
tag = "URL",
|
|
65
|
+
annotation = n.attributes["href"]!!,
|
|
66
|
+
start = n.child!!.pos.start,
|
|
67
|
+
end = n.child!!.pos.end,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
22
|
-
|
|
73
|
+
ClickableText(
|
|
23
74
|
modifier = Modifier.padding(bottom = 12.dp),
|
|
24
75
|
style = TextStyle(
|
|
25
76
|
fontFamily = fontType.family(),
|
|
@@ -27,28 +78,22 @@ fun VerseHeading(
|
|
|
27
78
|
fontWeight = FontWeight.W700,
|
|
28
79
|
color = MaterialTheme.colorScheme.onSurface,
|
|
29
80
|
),
|
|
30
|
-
text =
|
|
81
|
+
text = annotatedString,
|
|
31
|
-
// text = AnnotatedString.fromHtml(
|
|
32
|
-
// htmlString = text,
|
|
33
|
-
// linkStyles = TextLinkStyles(
|
|
34
|
-
// style = SpanStyle(
|
|
35
|
-
// fontSize = (14 + fontSizeDelta).sp,
|
|
36
|
-
// fontStyle = FontStyle.Italic,
|
|
37
|
-
// color = Color(0xFF008AE6),
|
|
38
|
-
// )
|
|
39
|
-
|
|
82
|
+
onClick = {
|
|
40
|
-
// linkInteractionListener = {
|
|
41
|
-
//
|
|
83
|
+
// view.playSoundEffect(SoundEffectConstants.CLICK)
|
|
84
|
+
annotatedString
|
|
42
|
-
|
|
85
|
+
.getStringAnnotations("URL", it, it)
|
|
86
|
+
.map { anno ->
|
|
43
|
-
|
|
87
|
+
val parts = anno.item.split(":")
|
|
44
|
-
|
|
88
|
+
navController.navigate(
|
|
45
|
-
|
|
89
|
+
ChapterScreenProps(
|
|
46
|
-
|
|
90
|
+
bookIndex = parts[0].toInt(),
|
|
47
|
-
|
|
91
|
+
chapterIndex = parts[1].toInt(),
|
|
48
|
-
|
|
92
|
+
verseIndex = parts[2].toInt(),
|
|
49
|
-
// )
|
|
50
|
-
// )
|
|
51
|
-
// },
|
|
52
|
-
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
}
|
|
53
98
|
)
|
|
54
99
|
}
|
composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/composables/VerseText.kt
CHANGED
|
@@ -43,6 +43,7 @@ import androidx.compose.ui.layout.positionInRoot
|
|
|
43
43
|
import androidx.compose.ui.text.SpanStyle
|
|
44
44
|
import androidx.compose.ui.text.TextStyle
|
|
45
45
|
import androidx.compose.ui.text.buildAnnotatedString
|
|
46
|
+
import androidx.compose.ui.text.font.FontStyle
|
|
46
47
|
import androidx.compose.ui.text.font.FontWeight
|
|
47
48
|
import androidx.compose.ui.text.withStyle
|
|
48
49
|
import androidx.compose.ui.unit.IntOffset
|
|
@@ -58,6 +59,9 @@ import kotlinx.coroutines.IO
|
|
|
58
59
|
import kotlinx.coroutines.launch
|
|
59
60
|
import dev.pyrossh.only_bible_app.lightHighlights
|
|
60
61
|
import dev.pyrossh.only_bible_app.rememberShareVerses
|
|
62
|
+
import dev.pyrossh.only_bible_app.utils.SimpleParser
|
|
63
|
+
import dev.pyrossh.only_bible_app.utils.TagNode
|
|
64
|
+
import dev.pyrossh.only_bible_app.utils.TextNode
|
|
61
65
|
import utils.LocalNavController
|
|
62
66
|
|
|
63
67
|
@Composable
|
|
@@ -78,15 +82,15 @@ fun VerseText(
|
|
|
78
82
|
val isSelected = selectedVerses.contains(verse)
|
|
79
83
|
val highlightedColorIndex = model.getHighlightForVerse(verse)
|
|
80
84
|
val currentHighlightColors = if (isLight) lightHighlights else darkHighlights
|
|
81
|
-
val currentHighlightWordKey = if (isLight) "background" else "color"
|
|
82
85
|
val text = if (highlightWord != null)
|
|
83
86
|
verse.text.replace(
|
|
84
87
|
highlightWord,
|
|
85
|
-
"<
|
|
88
|
+
"<yellow>${highlightWord}</yellow>",
|
|
86
89
|
true
|
|
87
90
|
)
|
|
88
91
|
else
|
|
89
92
|
verse.text
|
|
93
|
+
val nodes = SimpleParser(text).parse()
|
|
90
94
|
Text(
|
|
91
95
|
modifier = Modifier
|
|
92
96
|
.onPlaced {
|
|
@@ -141,22 +145,29 @@ fun VerseText(
|
|
|
141
145
|
) {
|
|
142
146
|
append("${verse.verseIndex + 1} ")
|
|
143
147
|
}
|
|
148
|
+
for (n in nodes) {
|
|
149
|
+
if (n is TextNode) {
|
|
144
|
-
|
|
150
|
+
append(n.value)
|
|
145
|
-
// append(
|
|
146
|
-
// AnnotatedString.Companion.fromHtml(
|
|
147
|
-
// htmlString = text,
|
|
148
|
-
// linkStyles = TextLinkStyles(
|
|
149
|
-
// style = SpanStyle(
|
|
150
|
-
// fontSize = (14 + model.fontSizeDelta).sp,
|
|
151
|
-
// fontStyle = FontStyle.Italic,
|
|
152
|
-
// color = Color(0xFF008AE6),
|
|
153
|
-
// )
|
|
154
|
-
// ),
|
|
155
|
-
|
|
151
|
+
} else if (n is TagNode && n.name == "br") {
|
|
152
|
+
append("\n")
|
|
153
|
+
} else if (n is TagNode && n.name == "red") {
|
|
154
|
+
withStyle(
|
|
155
|
+
style = SpanStyle(
|
|
156
|
+
color = Color.Red,
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
append(n.child!!.value)
|
|
160
|
+
}
|
|
156
|
-
|
|
161
|
+
} else if (n is TagNode && n.name == "yellow") {
|
|
157
|
-
|
|
162
|
+
withStyle(
|
|
158
|
-
|
|
163
|
+
style = SpanStyle(
|
|
159
|
-
|
|
164
|
+
background = Color.Yellow,
|
|
165
|
+
)
|
|
166
|
+
) {
|
|
167
|
+
append(n.child!!.value)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
160
171
|
}
|
|
161
172
|
)
|
|
162
173
|
if (isSelected && selectedVerses.last() == verse) {
|
composeApp/src/commonMain/kotlin/dev/pyrossh/only_bible_app/utils/SimpleParser.kt
CHANGED
|
@@ -66,16 +66,18 @@ class SimpleParser(val input: CharSequence) {
|
|
|
66
66
|
skip(4)
|
|
67
67
|
return TagNode(Pos(start, cursor), "br")
|
|
68
68
|
}
|
|
69
|
-
for (t in listOf("red", "yellow"
|
|
69
|
+
for (t in listOf("red", "yellow")) {
|
|
70
70
|
if (input.substring(start, start + 2 + t.length) == "<$t>") {
|
|
71
|
-
val redNode = TagNode(Pos(start, 0), t)
|
|
72
71
|
skip(t.length + 2)
|
|
73
72
|
val textNode = parseTextNode()
|
|
74
73
|
if (input.substring(textNode.pos.end, textNode.pos.end + t.length + 3) == "</$t>") {
|
|
75
74
|
skip(t.length + 3)
|
|
76
|
-
redNode.pos = Pos(start, textNode.pos.end + t.length + 3)
|
|
77
|
-
redNode.child = textNode
|
|
78
|
-
return
|
|
75
|
+
return TagNode(
|
|
76
|
+
Pos(start, textNode.pos.end + t.length + 3),
|
|
77
|
+
t,
|
|
78
|
+
emptyMap(),
|
|
79
|
+
textNode
|
|
80
|
+
)
|
|
79
81
|
} else {
|
|
80
82
|
throw RuntimeException(
|
|
81
83
|
"failed find closing tag for <red> at ${textNode.pos.end} ${
|
|
@@ -89,6 +91,57 @@ class SimpleParser(val input: CharSequence) {
|
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
if (input.substring(start, start + 2) == "<a") {
|
|
95
|
+
skip(2)
|
|
96
|
+
val attrs = parseAttributes()
|
|
97
|
+
if (consume() != '>') {
|
|
98
|
+
throw RuntimeException("failed to parseTag 'a' close '>'")
|
|
99
|
+
}
|
|
100
|
+
val textNode = parseTextNode()
|
|
101
|
+
if (input.substring(textNode.pos.end, textNode.pos.end + 4) == "</a>") {
|
|
102
|
+
skip(4)
|
|
103
|
+
return TagNode(Pos(start, textNode.pos.end + 4), "a", attrs, textNode)
|
|
104
|
+
} else {
|
|
105
|
+
throw RuntimeException(
|
|
106
|
+
"failed find closing tag for <a> ${textNode.pos.end}"
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
throw RuntimeException("failed to parseTag at $start ${input.substring(start, start + 10)}")
|
|
93
112
|
}
|
|
113
|
+
|
|
114
|
+
private fun parseAttributes(): Map<String, String> {
|
|
115
|
+
val start = cursor
|
|
116
|
+
val key = StringBuilder()
|
|
117
|
+
while (hasNext() && peek() != '=') {
|
|
118
|
+
while (peek() == ' ') {
|
|
119
|
+
skip()
|
|
120
|
+
}
|
|
121
|
+
key.append(consume())
|
|
122
|
+
}
|
|
123
|
+
skip()
|
|
124
|
+
while (peek() == ' ') {
|
|
125
|
+
skip()
|
|
126
|
+
}
|
|
127
|
+
val value = parseString()
|
|
128
|
+
if (peek() == '>') {
|
|
129
|
+
return mapOf(Pair(key.toString(), value))
|
|
130
|
+
}
|
|
131
|
+
throw RuntimeException("failed to parseAttribute at $start")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun parseString(): String {
|
|
135
|
+
val start = cursor
|
|
136
|
+
val result = StringBuilder()
|
|
137
|
+
if (consume() != '"') {
|
|
138
|
+
throw RuntimeException("failed to parseAttribute at ${start + 1}")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
while (hasNext() && peek() != '"') {
|
|
142
|
+
result.append(consume())
|
|
143
|
+
}
|
|
144
|
+
skip()
|
|
145
|
+
return result.toString()
|
|
146
|
+
}
|
|
94
147
|
}
|
composeApp/src/commonTest/kotlin/dev/pyrossh/only_bible_app/utils/SimpleParserTest.kt
CHANGED
|
@@ -9,7 +9,7 @@ class SimpleParserTest {
|
|
|
9
9
|
fun parseTextOnly() {
|
|
10
10
|
val parser = SimpleParser("lorem ipsum 123 dorem")
|
|
11
11
|
assertEquals(
|
|
12
|
-
listOf(TextNode(pos = Pos(0,
|
|
12
|
+
listOf(TextNode(pos = Pos(0, 21), value = "lorem ipsum 123 dorem")),
|
|
13
13
|
parser.parse(),
|
|
14
14
|
)
|
|
15
15
|
}
|
|
@@ -30,4 +30,35 @@ class SimpleParserTest {
|
|
|
30
30
|
parser.parse(),
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
@Test
|
|
35
|
+
fun parseTextA() {
|
|
36
|
+
val parser =
|
|
37
|
+
SimpleParser("The History of Creation <br> (<a href=\"42:0:0\">John 1:1–5</a> ; <a href=\"57:10:1\">Hebrews 11:1–3</a>)|In the beginning God created the heaven and the earth.")
|
|
38
|
+
assertEquals(
|
|
39
|
+
listOf(
|
|
40
|
+
TextNode(pos = Pos(start = 0, end = 24), value = "The History of Creation "),
|
|
41
|
+
TagNode(pos = Pos(start = 24, end = 28), name = "br"),
|
|
42
|
+
TextNode(pos = Pos(start = 28, end = 30), value = " ("),
|
|
43
|
+
TagNode(
|
|
44
|
+
pos = Pos(start = 30, end = 61),
|
|
45
|
+
name = "a",
|
|
46
|
+
attributes = mapOf("href" to "42:0:0"),
|
|
47
|
+
child = TextNode(pos = Pos(start = 47, end = 57), value = "John 1:1–5")
|
|
48
|
+
),
|
|
49
|
+
TextNode(pos = Pos(start = 61, end = 64), value = " ; "),
|
|
50
|
+
TagNode(
|
|
51
|
+
pos = Pos(start = 64, end = 100),
|
|
52
|
+
name = "a",
|
|
53
|
+
attributes = mapOf("href" to "57:10:1"),
|
|
54
|
+
child = TextNode(pos = Pos(start = 82, end = 96), value = "Hebrews 11:1–3")
|
|
55
|
+
),
|
|
56
|
+
TextNode(
|
|
57
|
+
pos = Pos(start = 100, end = 156),
|
|
58
|
+
value = ")|In the beginning God created the heaven and the earth."
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
parser.parse(),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
33
64
|
}
|
composeApp/src/iosMain/kotlin/dev/pyrossh/only_bible_app/Platform.ios.kt
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
package dev.pyrossh.only_bible_app
|
|
2
2
|
|
|
3
|
+
import androidx.compose.foundation.isSystemInDarkTheme
|
|
3
4
|
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
4
6
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
7
|
+
import androidx.compose.ui.graphics.toArgb
|
|
5
8
|
import androidx.compose.ui.platform.LocalWindowInfo
|
|
6
9
|
import androidx.compose.ui.unit.Dp
|
|
7
10
|
import androidx.compose.ui.unit.dp
|
|
8
11
|
import dev.pyrossh.only_bible_app.config.BuildKonfig
|
|
9
12
|
import dev.pyrossh.only_bible_app.domain.Verse
|
|
10
13
|
import platform.UIKit.UIScreen
|
|
14
|
+
import theme.darkScheme
|
|
15
|
+
import theme.lightScheme
|
|
16
|
+
|
|
11
17
|
//import platform.AVKit.Audio
|
|
12
18
|
|
|
13
19
|
@OptIn(ExperimentalComposeUiApi::class)
|
|
@@ -26,7 +32,14 @@ actual fun playClickSound() {
|
|
|
26
32
|
// AudioServicesPlayAlertSound(SystemSoundID(1322))
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
@Composable
|
|
29
|
-
actual fun
|
|
36
|
+
actual fun rememberShareVerses(): (verses: List<Verse>) -> Unit {
|
|
37
|
+
return { verses ->
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Composable
|
|
42
|
+
actual fun onThemeChange(themeType: ThemeType) {
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
actual object SpeechService {
|